From efdb99f8cecc4afb592afad79c761081d5d5cf22 Mon Sep 17 00:00:00 2001 From: lee <4766465@qq.com> Date: Wed, 18 Dec 2024 13:27:00 +0800 Subject: [PATCH] init --- xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java | 159 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java | 158 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml | 87 xxl-job/doc/images/img_Ypik.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java | 220 xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl | 188 xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js | 4 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java | 260 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java | 22 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js | 398 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java | 129 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java | 50 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java | 369 xxl-job/doc/images/img_UDSo.png | 0 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif | 0 xxl-job/xxl-job-admin/Dockerfile | 11 xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties | 276 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java | 35 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java | 23 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java | 28 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java | 38 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java | 55 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java | 179 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java | 67 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java | 252 xxl-job/doc/images/img_Qohm.png | 0 xxl-job/doc/images/donate-paypal.png | 0 xxl-job/doc/images/img_Z9Qr.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java | 48 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties | 26 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java | 45 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobRegistryDaoTest.java | 30 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java | 144 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java | 80 xxl-job/doc/images/img_jrdI.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java | 156 xxl-job/doc/images/img_Hr2T.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java | 31 xxl-job/doc/images/img_6yC0.png | 0 xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js | 117 xxl-job/doc/images/img_o8HQ.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java | 28 xxl-job/doc/images/img_iUw0.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java | 98 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java | 32 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobInfoDaoTest.java | 86 xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl | 164 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java | 122 xxl-job/doc/images/img_eYrv.png | 0 xxl-job/doc/images/img_ZAhX.png | 0 xxl-job/doc/images/donate-alipay.jpg | 0 xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js | 1106 + xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js | 234 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java | 77 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java | 101 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java | 126 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js | 152 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java | 48 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java | 172 xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl | 31 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java | 92 xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl | 72 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java | 47 xxl-job/doc/images/xxl-logo.png | 0 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java | 14 xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js | 91 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java | 62 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif | 0 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css | 1 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties | 17 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java | 58 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java | 28 xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl | 172 xxl-job/xxl-job-admin/pom.xml | 113 xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl | 45 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/mvc/controller/IndexController.java | 18 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java | 93 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js | 879 + xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java | 54 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java | 48 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java | 46 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java | 66 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java | 48 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java | 413 xxl-job/doc/images/img_tJOq.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java | 72 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml | 273 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobGroupDaoTest.java | 44 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile | 11 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css | 36 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java | 16 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml | 62 xxl-job/doc/images/img_V3vF.png | 0 xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java | 53 xxl-job/doc/images/xxl-logo.jpg | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java | 110 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java | 271 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogGlueDaoTest.java | 36 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css | 62 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java | 65 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java | 256 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js | 409 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java | 27 xxl-job/doc/XXL-JOB官方文档.md | 2324 +++ xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties | 276 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java | 233 xxl-job/doc/images/qq群-一个xxl同学进了58.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java | 14 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java | 37 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java | 181 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java | 25 xxl-job/doc/XXL-JOB架构图.pptx | 0 xxl-job/doc/images/img_dNUJ.png | 0 xxl-job/xxl-job-core/pom.xml | 64 xxl-job/doc/images/img_1001.png | 0 xxl-job/doc/images/img_tvGI.png | 0 xxl-job/doc/images/img_EB65.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java | 76 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png | 0 xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js | 1106 + xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java | 24 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml | 71 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java | 79 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml | 91 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml | 45 xxl-job/doc/images/img_1002.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java | 30 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/FramelessApplication.java | 38 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java | 29 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java | 150 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java | 124 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java | 79 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java | 88 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java | 180 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java | 53 xxl-job/xxl-job-admin/src/main/resources/logback.xml | 29 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java | 96 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java | 38 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java | 90 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java | 86 xxl-job/doc/db/tables_xxl_job.sql | 122 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java | 85 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml | 29 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java | 57 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java | 24 xxl-job/xxl-job-executor-samples/pom.xml | 18 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java | 1666 ++ xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js | 899 + xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml | 240 xxl-job/doc/images/gitee-gvp.jpg | 0 xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties | 276 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java | 13 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js | 9698 ++++++++++++++ xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java | 93 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java | 16 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java | 31 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java | 12 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/adminbiz/AdminBizTest.java | 75 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java | 42 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css | 346 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java | 157 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java | 204 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java | 99 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java | 253 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js | 41 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java | 73 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java | 75 xxl-job/xxl-job-admin/src/main/resources/static/js/index.js | 207 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png | 0 xxl-job/doc/images/img_Fgql.png | 0 xxl-job/doc/images/img_Wb2o.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java | 19 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js | 434 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java | 46 xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js | 97 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java | 118 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java | 24 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java | 50 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml | 74 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js | 10 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java | 39 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml | 27 xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl | 540 xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl | 180 xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js | 156 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java | 73 xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js | 22 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java | 35 xxl-job/doc/images/cnblog-首页-每日一博-第一.png | 0 xxl-job/doc/XXL-JOB-English-Documentation.md | 1247 + xxl-job/doc/images/donate-wechat.png | 0 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js | 2 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java | 46 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java | 59 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java | 434 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml | 62 xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js | 739 + xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java | 96 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java | 251 xxl-job/doc/images/cnblog-首页-热门动弹-第一.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java | 184 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogDaoTest.java | 52 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java | 107 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/executorbiz/ExecutorBizTest.java | 105 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java | 19 xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js | 66 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java | 255 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java | 152 xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java | 40 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java | 75 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java | 228 xxl-job/doc/images/img_jOAU.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java | 197 xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js | 396 xxl-job/doc/images/img_ZAsz.png | 0 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java | 237 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java | 24 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java | 54 xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl | 47 xxl-job/xxl-job-admin/src/main/resources/application.properties | 65 xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js | 328 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java | 226 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java | 38 xxl-job/doc/images/img_hIci.png | 0 xxl-job/doc/images/img_inc8.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java | 203 xxl-job/doc/images/img_BPLG.png | 0 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java | 56 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java | 26 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java | 56 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java | 133 xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java | 78 xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js | 359 xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl | 147 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java | 20 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java | 49 xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl | 239 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java | 70 242 files changed, 40,360 insertions(+), 0 deletions(-) diff --git a/xxl-job/doc/XXL-JOB-English-Documentation.md b/xxl-job/doc/XXL-JOB-English-Documentation.md new file mode 100644 index 0000000..792f3ea --- /dev/null +++ b/xxl-job/doc/XXL-JOB-English-Documentation.md @@ -0,0 +1,1247 @@ +## 《Distributed task scheduling framework XXL-JOB》 + +[![Actions Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/) +[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-job.svg)](https://github.com/xuxueli/xxl-job/releases) +[![GitHub stars](https://img.shields.io/github/stars/xuxueli/xxl-job)](https://github.com/xuxueli/xxl-job/) +[![Docker Status](https://img.shields.io/docker/pulls/xuxueli/xxl-job-admin)](https://hub.docker.com/r/xuxueli/xxl-job-admin/) +[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html) +[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat)](https://www.xuxueli.com/page/donate.html) + +[TOCM] + +[TOC] + +## 1. Brief introduction + +### 1.1 Overview +XXL-JOB is a distributed task scheduling framework, the core design goal is to develop quickly, learning simple, lightweight, easy to expand. Is now open source and access to a number of companies online product line, download and use it now. + +> English document update slightly delayed, Please check the Chinese version for the latest document. + +### 1.2 Features +- 1.Simple: support through the Web page on the task CRUD operation, simple operation, a minute to get started; +- 2.Dynamic: support dynamic modification of task status, pause / resume tasks, and termination of running tasks,immediate effect; +- 3.Dispatch center HA (center type): Dispatch with central design, "dispatch center" based on the cluster of Quartz implementation, can guarantee the scheduling - center HA; +- 4.Executor HA (Distributed): Task Distributed Execution, Task " Executer " supports cluster deployment to ensure that tasks perform HA; +- 5.Task Failover: Deploy the Excutor cluster,tasks will be smooth to switch excuter when the strategy of the router choose ‘failover’; +- 6.Consistency: "Dispatch Center" through the DB lock to ensure the consistency of cluster distributed scheduling,one task excuted for once; +- 7.Custom task parameters: support online configuration scheduling tasks into the parameters, immediate effect; +- 8.Scheduling thread pool: scheduling system multi-threaded trigger scheduling operation, to ensure accurate scheduling, not blocked; +- 9.Elastic expansion capacity: once the new executor machine on the line or off the assembly line, the next time scheduling will be re-assigned tasks; +- 10.Mail alarm: the task fails to support e-mail alarm, support configuring multiple email addresses to send bulk alert messages; +- 11.Status monitoring: support real-time monitoring of the progress of the task; +- 12.Rolling execution log: support online view scheduling results, and support Rolling real-time view of the executer output of the complete implementation of the log; +- 13.GLUE: provide Web IDE, support online development task logic code, dynamic release, real-time compiler effective, omit the deployment of the on-line process. Supports historical versions of 30 versions back; +- 14.Data Encryption: The communication between the dispatching center and the executor is used for data encryption, Enhancing the security of dispatching information; +- 15.Task Dependency: Support configuration subtask dependencies, When the parent task executed end and after the success of the implementation will take the initiative to trigger a second task execution, multiple sub tasks are separated by commas; +- 16.Push the Maven central warehouse: The latest stable version will be sent to the Maven central warehouse to facilitate user access and use; +- 17.Task registration: The executor automatically registers tasks periodically, and the dispatch center automatically finds the registered tasks and triggers execution. It also supports manual input of executor address; +- 18.Router strategy: A rich routing strategy is provided when the executor cluster is deployed, these include: first, last, poll, random, consistent HASH, least frequently used, least recently used, failover, busy over, sharding broadcast,etc.; +- 19.Report monitor: Support real-time view of running data, such as the number of tasks, the number of dispatch, the number of executors, etc .; and scheduling reports, such as scheduling date distribution, scheduling success map; +- 20.Script task: Support the development and operation of script tasks in GLUE mode, including shell, Python and other types of script; +- 21.Blocking handling strategy: The scheduling is too dense and the executor is too late to handle. The strategy includes: single machine serial (default), discarding the following scheduling, and Override the previous scheduling; +- 22.Failure handling strategy:Handling strategy when scheduling fails, the strategy includes: failure alarm (default), failure retry; +- 23.Sharding broadcast task: When an executor cluster is deployed, task routing strategy select "sharding broadcast", a task schedule will broadcast all the actuators in the cluster to perform it once, you can develop sharding tasks based on sharding parameters; +- 24.Dynamic sharding: The sharding broadcast task is sharded by the executors to support the dynamic expansion of the executor cluster to dynamically increase the number of shardings and cooperate with the business handle; In the large amount of data operations can significantly improve the task processing capacity and speed. +- 25、Event trigger:In addition to "Cron" and "Task Dependency" to trigger tasks, support event-based triggering tasks. The dispatch center provides API service that triggers a single execution of the task, it can be triggered flexibly according to business events. + + +### 1.3 Development +In 2015, I created the XXL-JOB project repository on github and submitted the first commit, followed by the system structure design, UI selection, interactive design ... +In 2015 - November, XXL-JOB finally RELEASE the first big version of V1.0, then I will be released to OSCHINA, XXL-JOB OSCHINA won the popular recommendation of @红薯, the same period reached OSCHINA's " Popular move "ranked first and git.oschina open source software monthly heat ranked first, especially thanks for @红薯, thank you for the attention and support. +In 2015 - December, I will XXL-JOB published to our internal knowledge base, and get internal colleagues recognized. +In 2016 - 01 months, my company started XXL-JOB internal access and custom work, in this thank Yuan and Yin two colleagues contribution, but also to thank the internal other attention and support colleagues. +In 2017-05-13, the link of "let the code run" in "[the 62nd source of open source China Genesis](https://www.oschina.net/event/2236961)" held in Shanghai,, I stepped on and made a speech about the XXL-JOB, five hundred spectators in the audience reacted enthusiastically ([pictorial review](https://www.oschina.net/question/2686220_2242120)). +> Our company have access to XXL-JOB, internal alias "Ferrari" (Ferrari based on XXL-JOB V1.1 version customization, new access application recommended to upgrade the latest version). +According to the latest statistics, from 2016-01-21 to 2017-07-07 period, the system has been scheduled about 600,000 times, outstanding performance. New access applications recommend the latest version, because after several major updates, the system's task model, UI interaction model and the underlying scheduling communication model has a greater optimization and upgrading, the core function more stable and efficient. + +So far, XXL-JOB has access to a number of companies online product line, access to scenes such as electronic commerce, O2O business and large data operations, as of 2016-07-19, XXL-JOB has access to the company But not limited to: + + - 1、大众点评【美团点评】 + - 2、山东学而网络科技有限公司; + - 3、安徽慧通互联科技有限公司; + - 4、人人聚财金服; + - 5、上海棠棣信息科技股份有限公司 + - 6、运满满【运满满】 + - 7、米其林 (中国区)【米其林】 + - 8、妈妈联盟 + - 9、九樱天下(北京)信息技术有限公司 + - 10、万普拉斯科技有限公司【一加手机】 + - 11、上海亿保健康管理有限公司 + - 12、海尔馨厨【海尔】 + - 13、河南大红包电子商务有限公司 + - 14、成都顺点科技有限公司 + - 15、深圳市怡亚通 + - 16、深圳麦亚信科技股份有限公司 + - 17、上海博莹科技信息技术有限公司 + - 18、中国平安科技有限公司【中国平安】 + - 19、杭州知时信息科技有限公司 + - 20、博莹科技(上海)有限公司 + - 21、成都依能股份有限责任公司 + - 22、湖南高阳通联信息技术有限公司 + - 23、深圳市邦德文化发展有限公司 + - 24、福建阿思可网络教育有限公司 + - 25、优信二手车【优信】 + - 26、上海悠游堂投资发展股份有限公司【悠游堂】 + - 27、北京粉笔蓝天科技有限公司 + - 28、中秀科技(无锡)有限公司 + - 29、武汉空心科技有限公司 + - 30、北京蚂蚁风暴科技有限公司 + - 31、四川互宜达科技有限公司 + - 32、钱包行云(北京)科技有限公司 + - 33、重庆欣才集团 + - 34、咪咕互动娱乐有限公司【中国移动】 + - 35、北京诺亦腾科技有限公司 + - 36、增长引擎(北京)信息技术有限公司 + - 37、北京英贝思科技有限公司 + - 38、刚泰集团 + - 39、深圳泰久信息系统股份有限公司 + - 40、随行付支付有限公司 + - 41、广州瀚农网络科技有限公司 + - 42、享点科技有限公司 + - 43、杭州比智科技有限公司 + - 44、圳临界线网络科技有限公司 + - 45、广州知识圈网络科技有限公司 + - 46、国誉商业上海有限公司 + - 47、海尔消费金融有限公司,嗨付、够花【海尔】 + - 48、广州巴图鲁信息科技有限公司 + - 49、深圳市鹏海运电子数据交换有限公司 + - 50、深圳市亚飞电子商务有限公司 + - 51、上海趣医网络有限公司 + - 52、聚金资本 + - 53、北京父母邦网络科技有限公司 + - 54、中山元赫软件科技有限公司 + - 55、中商惠民(北京)电子商务有限公司 + - 56、凯京集团 + - 57、华夏票联(北京)科技有限公司 + - 58、拍拍贷【拍拍贷】 + - 59、北京尚德机构在线教育有限公司 + - 60、任子行股份有限公司 + - 61、北京时态电子商务有限公司 + - 62、深圳卷皮网络科技有限公司 + - 63、北京安博通科技股份有限公司 + - 64、未来无线网 + - 65、厦门瓷禧网络有限公司 + - 66、北京递蓝科软件股份有限公司 + - 67、郑州创海软件科技公司 + - 68、北京国槐信息科技有限公司 + - 69、浪潮软件集团 + - 70、多立恒(北京)信息技术有限公司 + - 71、广州极迅客信息科技有限公司 + - 72、赫基(中国)集团股份有限公司 + - 73、海投汇 + - 74、上海润益创业孵化器管理股份有限公司 + - 75、汉纳森(厦门)数据股份有限公司 + - 76、安信信托 + - 77、岚儒财富 + - 78、捷道软件 + - 79、湖北享七网络科技有限公司 + - 80、湖南创发科技责任有限公司 + - 81、深圳小安时代互联网金融服务有限公司 + - 82、湖北享七网络科技有限公司 + - 83、钱包行云(北京)科技有限公司 + - 84、360金融【360】 + - 85、易企秀 + - 86、摩贝(上海)生物科技有限公司 + - 87、广东芯智慧科技有限公司 + - 88、联想集团【联想】 + - 89、怪兽充电 + - 90、行圆汽车 + - 91、深圳店店通科技邮箱公司 + - 92、京东【京东】 + - 93、米庄理财 + - 94、咖啡易融 + - 95、梧桐诚选 + - 96、恒大地产【恒大】 + - 97、昆明龙慧 + - 98、上海涩瑶软件 + - 99、易信【网易】 + - 100、铜板街 + - 101、杭州云若网络科技有限公司 + - 102、特百惠(中国)有限公司 + - 103、常山众卡运力供应链管理有限公司 + - 104、深圳立创电子商务有限公司 + - 105、杭州智诺科技股份有限公司 + - 106、北京云漾信息科技有限公司 + - 107、深圳市多银科技有限公司 + - 108、亲宝宝 + - 109、上海博卡软件科技有限公司 + - 110、智慧树在线教育平台 + - 111、米族金融 + - 112、北京辰森世纪 + - 113、云南滇医通 + - 114、广州市分领网络科技有限责任公司 + - 115、浙江微能科技有限公司 + - 116、上海馨飞电子商务有限公司 + - 117、上海宝尊电子商务有限公司 + - 118、直客通科技技术有限公司 + - 119、科度科技有限公司 + - 120、上海数慧系统技术有限公司 + - 121、我的医药网 + - 122、多粉平台 + - 123、铁甲二手机 + - 124、上海海新得数据技术有限公司 + - 125、深圳市珍爱网信息技术有限公司【珍爱网】 + - 126、小蜜蜂 + - 127、吉荣数科技 + - 128、上海恺域信息科技有限公司 + - 129、广州荔支网络有限公司【荔枝FM】 + - 130、杭州闪宝科技有限公司 + - 131、北京互联新网科技发展有限公司 + - 132、誉道科技 + - 133、山西兆盛房地产开发有限公司 + - 134、北京蓝睿通达科技有限公司 + - 135、月亮小屋(中国)有限公司【蓝月亮】 + - 136、青岛国瑞信息技术有限公司 + - 137、博雅云计算(北京)有限公司 + - 138、华泰证券香港子公司 + - 139、杭州东方通信软件技术有限公司 + - 140、武汉博晟安全技术股份有限公司 + - 141、深圳市六度人和科技有限公司 + - 142、杭州趣维科技有限公司(小影) + - 143、宁波单车侠之家科技有限公司【单车侠】 + - 144、丁丁云康信息科技(北京)有限公司 + - 145、云钱袋 + - 146、南京中兴力维 + - 147、上海矽昌通信技术有限公司 + - 148、深圳萨科科技 + - 149、中通服创立科技有限责任公司 + - 150、深圳市对庄科技有限公司 + - 151、上证所信息网络有限公司 + - 152、杭州火烧云科技有限公司【婚礼纪】 + - 153、天津青芒果科技有限公司【芒果头条】 + - 154、长飞光纤光缆股份有限公司 + - 155、世纪凯歌(北京)医疗科技有限公司 + - 156、浙江霖梓控股有限公司 + - 157、江西腾飞网络技术有限公司 + - 158、安迅物流有限公司 + - 159、肉联网 + - 160、北京北广梯影广告传媒有限公司 + - 161、上海数慧系统技术有限公司 + - 162、大志天成 + - 163、上海云鹊医 + - 164、上海云鹊医 + - 165、墨迹天气【墨迹天气】 + - 166、上海逸橙信息科技有限公司 + - 167、沅朋物联 + - 168、杭州恒生云融网络科技有限公司 + - 169、绿米联创 + - 170、重庆易宠科技有限公司 + - 171、安徽引航科技有限公司(乐职网) + - 172、上海数联医信企业发展有限公司 + - 173、良彬建材 + - 174、杭州求是同创网络科技有限公司 + - 175、荷马国际 + - 176、点雇网 + - 177、深圳市华星光电技术有限公司 + - 178、厦门神州鹰软件科技有限公司 + - 179、深圳市招商信诺人寿保险有限公司 + - 180、上海好屋网信息技术有限公司 + - 181、海信集团【海信】 + - 182、信凌可信息科技(上海)有限公司 + - 183、长春天成科技发展有限公司 + - 184、用友金融信息技术股份有限公司【用友】 + - 185、北京咖啡易融有限公司 + - 186、国投瑞银基金管理有限公司 + - 187、晋松(上海)网络信息技术有限公司 + - 188、深圳市随手科技有限公司【随手记】 + - 189、深圳水务科技有限公司 + - 190、易企秀【易企秀】 + - 191、北京磁云科技 + - 192、南京蜂泰互联网科技有限公司 + - 193、章鱼直播 + - 194、奖多多科技 + - 195、天津市神州商龙科技股份有限公司 + - 196、岩心科技 + - 197、车码科技(北京)有限公司 + - 198、贵阳市投资控股集团 + - 199、康旗股份 + - 200、龙腾出行 + - 201、杭州华量软件 + - 202、合肥顶岭医疗科技有限公司 + - 203、重庆表达式科技有限公司 + - 204、上海米道信息科技有限公司 + - 205、北京益友会科技有限公司 + - 206、北京融贯电子商务有限公司 + - 207、中国外汇交易中心 + - 208、中国外运股份有限公司 + - 209、中国上海晓圈教育科技有限公司 + - 210、普联软件股份有限公司 + - 211、北京科蓝软件股份有限公司 + - 212、江苏斯诺物联科技有限公司 + - 213、北京搜狐-狐友【搜狐】 + - 214、新大陆网商金融 + - 215、山东神码中税信息科技有限公司 + - 216、河南汇顺网络科技有限公司 + - 217、北京华夏思源科技发展有限公司 + - 218、上海东普信息科技有限公司 + - 219、上海鸣勃网络科技有限公司 + - 220、广东学苑教育发展有限公司 + - 221、深圳强时科技有限公司 + - 222、上海云砺信息科技有限公司 + - 223、重庆愉客行网络有限公司 + - 224、数云 + - 225、国家电网运检部 + - 226、杭州找趣 + - 227、浩鲸云计算科技股份有限公司 + - 228、科大讯飞【科大讯飞】 + - 229、杭州行装网络科技有限公司 + - 230、即有分期金融 + - 231、深圳法司德信息科技有限公司 + - 232、上海博复信息科技有限公司 + - 233、杭州云嘉云计算有限公司 + - 234、有家民宿(有家美宿) + - 235、北京赢销通软件技术有限公司 + - 236、浙江聚有财金融服务外包有限公司 + - 237、易族智汇(北京)科技有限公司 + - 238、合肥顶岭医疗科技开发有限公司 + - 239、车船宝(深圳)旭珩科技有限公司) + - 240、广州富力地产有限公司 + - 241、氢课(上海)教育科技有限公司 + - 242、武汉氪细胞网络技术有限公司 + - 243、杭州有云科技有限公司 + - 244、上海仙豆智能机器人有限公司 + - 245、拉卡拉支付股份有限公司【拉卡拉】 + - 246、虎彩印艺股份有限公司 + - 247、北京数微科技有限公司 + - 248、广东智瑞科技有限公司 + - 249、找钢网 + - 250、九机网 + - 251、杭州跑跑网络科技有限公司 + - 252、深圳未来云集 + - 253、杭州每日给力科技有限公司 + - 254、上海齐犇信息科技有限公司 + - 255、滴滴出行【滴滴】 + - 256、合肥云诊信息科技有限公司 + - 257、云知声智能科技股份有限公司 + - 258、南京坦道科技有限公司 + - 259、爱乐优(二手平台) + - 260、猫眼电影(私有化部署)【猫眼电影】 + - 261、美团大象(私有化部署)【美团大象】 + - 262、作业帮教育科技(北京)有限公司【作业帮】 + - 263、北京小年糕互联网技术有限公司 + - 264、山东矩阵软件工程股份有限公司 + - 265、陕西国驿软件科技有限公司 + - 266、君开信息科技 + - 267、村鸟网络科技有限责任公司 + - 268、云南国际信托有限公司 + - 269、金智教育 + - 270、珠海市筑巢科技有限公司 + - 271、上海百胜软件股份有限公司 + - 272、深圳市科盾科技有限公司 + - 273、哈啰出行 + - 274、途虎养车 + - 275、卡思优派人力资源集团 + - 276、南京观为智慧软件科技有限公司 + - 277、杭州城市大脑科技有限公司 + - 278、猿辅导 + - …… + +> The company that access and use this product is welcome to register at the [address](https://github.com/xuxueli/xxl-job/issues/1 ), only for product promotion. + +Welcome everyone's attention and use, XXL-JOB will also embrace changes, sustainable development. + +### 1.4 Download + +#### Documentation +- [中文文档](https://www.xuxueli.com/xxl-job/) +- [English Documentation](https://www.xuxueli.com/xxl-job/en/) + +#### Source repository address (The latest code will be released in the two git warehouse in the same time) + +Source repository address | Release Download +--- | --- +[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job) | [Download](https://github.com/xuxueli/xxl-job/releases) +[http://gitee.com/xuxueli0323/xxl-job](http://gitee.com/xuxueli0323/xxl-job) | [Download](http://gitee.com/xuxueli0323/xxl-job/releases) + +#### Center repository address (The latest Release version:1.8.1) +``` +<!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ --> +<dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>1.8.2</version> +</dependency> +``` + +#### Technical exchange group +- [社区交流](https://www.xuxueli.com/page/community.html) +- [Gitter](https://gitter.im/xuxueli/xxl-job) + +### 1.5 Environment +- JDK:1.7+ +- Servlet/JSP Spec:3.1/2.3 +- Tomcat:8.5.x/Jetty9.2.x +- Spring-boot:1.5.x/Spring4.x +- Mysql:5.6+ +- Maven:3+ + + +## 2. Quick Start + +### 2.1 Init database +Please download project source code,get db scripts and execute, it will generate 16 tables if succeed. + +The relative path of db scripts is as follows: + + /xxl-job/doc/db/tables_xxl_job.sql + +The xxl-job-admin can be deployed as a cluster,all nodes of the cluster must connect to the same mysql instance. + +If mysql instances is deployed in master-slave mode,all nodes of the cluster must connect to master instace. + +### 2.2 Compile +Source code is organized by maven,unzip it and structure is as follows: + + xxl-job-admin:schedule admin center + xxl-job-core:public common dependent library + xxl-job-executor:executor Sample(Select appropriate version of executor,Can be used directly,You can also refer to it and transform existing projects into executors) + :xxl-job-executor-sample-spring:Spring version,executors managed by Spring,general and recommend; + :xxl-job-executor-sample-springboot:Springboot version,executors managed by Springboot; + +### 2.3 Configure and delploy "Schedule Center" + + schedule center project:xxl-job-admin + target:Centralized management、Schedule and trigger task + +#### Step 1:Configure Schedule Center +Configure file’s path of schedule center is as follows: + + /xxl-job/xxl-job-admin/src/main/resources/application.properties + + +The concrete contet describe as follows: + + ### JDBC connection info of schedule center:keep Consistent with chapter 2.1 + xxl.job.db.driverClass=com.mysql.jdbc.Driver + xxl.job.db.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai + xxl.job.db.user=root + xxl.job.db.password=root_pwd + + ### Alarm mailbox + xxl.job.mail.host=smtp.163.com + xxl.job.mail.port=25 + xxl.job.mail.username=ovono802302@163.com + xxl.job.mail.password=asdfzxcv + xxl.job.mail.sendFrom=ovono802302@163.com + xxl.job.mail.sendNick=《任务调度平台XXL-JOB》 + + ### Login account + xxl.job.login.username=admin + xxl.job.login.password=123456 + + ### TOKEN used for communication between the executor and schedule center, enabled if it’s not null + xxl.job.accessToken= + + ### Internationalized Settings, the default is Chinese version,Switch to English when the value is "en". + xxl.job.i18n=en + +#### Step 2:Deploy: +If you has finished step 1,then you can compile the project in maven and deploy the war package to tomcat. +the url to visit is :http://localhost:8080/xxl-job-admin (this address will be used by executor and use it as callback url),the index page after login in is as follow + +![index page after login in](https://www.xuxueli.com/doc/static/xxl-job/images/img_6yC0.png "index page after login in") + +Now,the “xxl-job-admin” project is deployed success. + +#### Step3:schedule center Cluster(Option): +xxl-job-admin can be deployed as a cluster to improve system availability. + +Prerequisites for cluster is to keep all node configuration(db and login account info) consistent with each other. Different xxl-job-admin cluster distinguish with each other by db configuration. + +xxl-job-admin can be visited through nginx proxy and configure a domain for nginx,and the domain url can be configured as the executor’s callback url. + +### 2.4 Configur and Deploy "xxl-job-executor-example" + + Executor Project:xxl-job-executor-example (if you want to create new executor project you can refer this demo); + Target:receive xxl-job-admin’s schedule command and execute it; + +#### Step 1:import maven dependence +Pleast confirm import xxl-job-core jar in pom.xml; + +#### Step 2:Executor Configuration +Relative path of the executor configuration file is as follows: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-spring/src/main/resources/xxl-job-executor.properties + +The concret content of configuration file as follows: + + ### xxl-job admin address list:xxl-job-admin address list: Multiple addresses are separated by commas,this address is used for "heart beat and register" and "task execution result callback" between the executor and xxl-job-admin. + xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + + ### xxl.job.executor.appname is used to group by executors + xxl.job.executor.appname=xxl-job-executor-sample + ### xxl.job.executor.ip :1,used to register with xxl-job-admin;2,xxl-job-admin dispatch task to executor through it;3,if it is blank executor will get ip automatically, multi network card need to be configured. + xxl.job.executor.ip= + ### xxl.job.executor.port :the port of the executor runned by,if multiple executor instance run on the same computer the port must different with each other + xxl.job.executor.port=9999 + + ### xxl-job log path:runtime log path of the job instance + xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler/ + + ### xxl-job, access token:xxl-job access token,enabled if it not blank + xxl.job.accessToken= + + +#### Step 3:executor configuration + +configure file path of executor: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-spring/src/main/resources/applicationcontext-xxl-job.xml + +Concrete contet describe as follows: + +``` +<!-- configure 01、JobHandler scan path:auto scan JobHandler bean managed by container --> +<context:component-scan base-package="com.xxl.job.executor.service.jobhandler" /> + +<!-- configure 02、Excutor:executer core configure --> +<bean id="xxlJobExecutor" class="com.xxl.job.core.executor.XxlJobExecutor" init-method="start" destroy-method="destroy" > + <!-- executor IP[required],auto get if it blank --> + <property name="ip" value="${xxl.job.executor.ip}" /> + <!-- executor port[required] --> + <property name="port" value="${xxl.job.executor.port}" /> + <!-- executor AppName[required],auto register will be closed if it blank --> + <property name="appname" value="${xxl.job.executor.appname}" /> + <!-- register center address of executor [required],auto register will be closed if it blank --> + <property name="adminAddresses" value="${xxl.job.admin.addresses}" /> + <!-- log path of executor[required] --> + <property name="logPath" value="${xxl.job.executor.logpath}" /> + <!-- access token, match check enabled if it not blank[required] --> + <property name="accessToken" value="${xxl.job.accessToken}" /> +</bean> +``` + +#### Step 4:deploy executor project +You can compile and package the project If have done all the steps above successfully,the project supply two executor demo projects,you can choose any one to deploy: + + xxl-job-executor-sample-spring:compile and package in WAR,can be deployed to tomcat; + xxl-job-executor-sample-springboot:compile and package in JAR,and run in springboot mode; + +Now you have deployed the executor project. + +#### Step 5:executor cluster(optional) +In order to improve system availability and job process capacity,executor project can be deployed as cluster. + +Prerequisites:keep all node’s configuration item "xxl.job.admin.addresses" exactly the same with each other,all executors can be register automatically. + + +### 2.5 Start first job "Hello World" +Now let’s create a "GLUE模式(Java)" job,if you want to learn more about it , please see “chapter 3:Task details”。( "GLUE模式(Java)"'s code is maintained online through xxl-job-admin,compare with "Bean模式任务" it’s not need to develop, deploy the code on the executor and it’s not need to restart the executor, so it’s lightweight) + +#### Prerequisites:please confirm xxl-job-admin and executor project has been deployed successfully. + +#### Step 1:Create new job +Login in xxl-job-admin,click on the"新建任务" button, configure the job params as follows and click "保存" button to save the job info. + +![task management](https://www.xuxueli.com/doc/static/xxl-job/images/img_o8HQ.png "task management") + +![create task](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "create task") + +#### Step 2:develop “GLUE模式(Java)” job +Click “GLUE” button on the right of the job to go to GLUE editor view as shown below。“GLUE模式(Java)” mode task has been inited with default task code for printing Hello World。 ( “GLUE模式(Java)” mode task is a java code fragment implements IJobHandler interface,it will be executed in executor,you can use @Resource/@Autowire to inject other java bean instance,if you want to see more info please go to chapter 3) + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Fgql.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +#### Step 3:trigger task +If you want to run the job manually please click "执行" button on the right of the job(usually we trigger job by Cron expression) + +#### Step 4:view log +Click “日志” button on the right side of the task you will go to the task log list ,you will see the schedule history records of the task and the schedule detail info,execution info and execution params.If you click the “执行日志” button on the right side of the task log record,you will go to log console and view the execute log in the course of task execution. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题") + +On the log console,you can view task execution log on the executor immediately after it dump to log file,so you can monitor the task execution process by Rolling way. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_eYrv.png "在这里输入图片标题") + +## 3. Task details + +### Description of configuration item: + + - 执行器:the container where job executed in,it will be discovered automaticly if it has registered success when job was scheduled,and the job will be executed automaticly through this way.On the other side all tasks was grouped by this way.Tasks must be binded to a executor and it can be configured on "执行器管理" page; + - 描述:the decription of task + - 路由策略:when executors deployed as a cluster,it can configure multi route policys,include: + FIRST(第一个):default select the first executor; + LAST(最后一个):default select the last executor; + ROUND(轮询):round select the executor;; + RANDOM(随机):random select the executor; + CONSISTENT_HASH(一致性HASH):all jobs was evenly scheduled on different machines,make sure load balance of executors under the same group and the same job will be scheduled to the same machine. + LEAST_FREQUENTLY_USED(最不经常使用):default select the least often used executor. + LEAST_RECENTLY_USED(最近最久未使用):defalut select the longest not used executor. + FAILOVER(故障转移):beat with the executor in order and select the first beat success executor as target executor. + BUSYOVER(忙碌转移):check the executor busy or not in order,the first executor checked not busy is to be select as the target scheduled executor. + SHARDING_BROADCAST(分片广播):broadcast all executor nodes under the same executor group execute the job, slice number will be transferred at the same time,shard task will be executed accordate with the shard number. + + - Cron:Cron expression used to trigger job execution; + - 运行模式: + BEAN模式:job was maintained on the side of executor by as JobHandler instance,it will be executed accordate with "JobHandler" properties. + GLUE模式(Java):task source code is maintened in the schedule center,it must implement IJobHandler and explain by "groovy" in the executor instance,inject other bean instace by annotation @Resource/@Autowire. + GLUE模式(Shell):it’s source code is a shell script and maintained in the schedule center. + GLUE模式(Python):it’s source code is a python script and maintained in the schedule center. + - JobHandler:it’s used in "BEAN模式",it’s instance is defined by annotation @JobHandler on the JobHandler class name. + - 子任务Key:every task has a unique key (task Key can acquire from task list),when main task is done successfully it’s child task stand for by this key will be scheduled. + - 阻塞处理策略:the stategy handle the task when this task is scheduled too frequently and the task is block to wait for cpu time. + 单机串行(默认):task schedule request go into the FIFO queue and execute serially. + 丢弃后续调度:the schedule request will be discarded and marked as fail when the same task’s instance scheduled befor is running in the target executor. + 覆盖之前调度:the schedule request will be executed and clear before task queue when the same task’s instance scheduled befor is running in the target executor. + - 失败处理策略:handle policy for schedule fail + 失败告警(默认):it will trigger alarm such as send alarm mail when it’s scheduled fail. + 失败重试:it will try another time when it’s scheduled fai,if try fail it will trigger alarm for fail.every time it will trigger a new schedule request. + - 执行参数:the params needed in the run time of the task, multiple values are separated by commas,it will be passed to task instace as an array when task is scheduled. + - 报警邮件:the email used to receive the alarm mail when task is scheduled fail or execute fail, multiple values are separated by commas. + - 负责人:The person name response for the task. + +### 3.1 BEAN模式 +The task logic exist in the executor project as JobHandler,the develop steps as shown below: + +#### Step 1:develp obHandler in the executor project + - 1, create new java class implent com.xxl.job.core.handler.IJobHandler; + - 2, if you add @Component annotation on the top of the class name it’s will be managed as a bean instance by spring container; + - 3, add “@JobHandler(value=" customize jobhandler name")” annotation,the value stand for JobHandler name,it will be used as JobHandler property when create a new task in the schedule center. + + +#### Step 2:create task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "BEAN模式" as run mode,property JobHandler please fill in the value defined by @JobHande. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题") + +### 3.2 GLUE模式(Java) +Task source code is maintained in the schedule center and can be updated by Web IDE online, it will be compiled and effective real-time,didn’t need to assign JobHandler,develop flow shown as below: + +#### Step 1:create task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Java)" as run mode. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tJOq.png "在这里输入图片标题") + +#### Step 2:develop task source code +Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page). + +Version backtrack(support 30 versions while backtrack):on the WEB IDE page of GLUE task,on upper right corner drop down box please select “版本回溯”,it will display GLUE updated history,select the version you want it will display the source code of this version,it will backtrace the version while click save button. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +### 3.3 GLUE模式(Shell) + +#### Step 1:create new task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Shell)"as run mode. + +#### Step 2:develop task source code +Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page). + +Actually it is a shell script fragment. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_iUw0.png "在这里输入图片标题") + +### 3.4 GLUE模式(Python) + +#### Step 1:create new task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Python)"as run mode. + +#### Step 2:develop task source code +Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page). + +Actually it is a python script fragment. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_BPLG.png "在这里输入图片标题") + + +## 4. Task Management +### 4.0 configure executor +click"执行器管理" on the left menu,it will go to the page as shown below: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Hr2T.png "在这里输入图片标题") + + 1,"调度中心OnLine”:display schedule center machine list,when task is scheduled it will callback schedule center for notify the execution result in failover mode, so that it can avoid a single point scheduler; + 2,"执行器列表" :display all nodes under this executor group. + +If you want to create a new executor,please click "+新增执行器" button: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_V3vF.png "在这里输入图片标题") + +### Description of executor attributes + + Appname: the unique identity of the executor cluster,executor will registe automatically and periodically by appname so that it can be scheduled. + 名称: the name of ther executor,it is used to describe the executor. + 排序: the order of executor,it will be used in the place where need to select executor. + 注册方式:which way the schedule center used to acquire executor address through; + 自动注册:executor will register automatically,through this schedule center can discover executor dynamically. + 手动录入:fill in executor address manually and it will be used by schedule center, multiple address separated by commas. + 机器地址:only effective when "注册方式" is "手动录入",support fill in executor address manually. + +### 4.1 create new task +Go to task management list page,click “新增任务” button on the upper right corner,on the pop-up window“新增任务”page configure task property and save.learn more info please go and see "3,task details". + +### 4.2 edit task +Go to task management list page and choose the task you want to edit ,click”编辑”button on the right side of the task,on the pop-up window “编辑任务”page edit task property and save. + +### 4.3 edit GLUE source code + +Only fit to GLUE task. + +choose the task you want to edit and click” GLUE”button on the right side of the task, it will go to the Web IDE page of GLUE task,then you can edit task source code on this page.you can read "3.2 GLUE模式(Java)" for more info. + +### 4.4 pause/recover task +You can pause or recover task but it just fit to follow up schedule trigger and won’t affect scheduled tasks,if you want to stop tasks which has been triggered,please go and see “4.8 stop the running task” + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +### 4.5 manually trigger +You can trigger a task manually by Click “执行”button,it won’t affect original scheduling rules. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z5wp.png "在这里输入图片标题") + +### 4.6 view schedule log +You can view task’s history schedule log by click “日志” button,on the history schedule log list page you can view every time of task’s schedule result,execution result and so on,click “执行日志” button can view the task’s full execute log. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_9235.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题") + + 调度时间:schedule center trigger time when schedule and send execution signal to executor; + 调度结果:schedule center trigger task’s result, 200 represent success,500 or other number stands for fail; + 调度备注:schedule center trigger task’s remark info; + 执行器地址:the machine address where the task was executed; + 运行模式:run mode of triggered task,go and see "3,Task Details" for more info; + 任务参数:the input params of the executed task; + 执行时间:the callback time task was done in the executor; + 执行结果:task’s execute result in the executor, 200 represent success,500 or other number stands for fail; + 执行备注:task’s execute remark info in the executor; + 操作: + "执行日志"button:click this button you can view task’s execution detail log,go and see chapter 4.7 “view execution log” for more info; + "终止任务"button:click this button you can stop the task’s execution thread on this executor,include bloked task instance which didn’t has started; + +### 4.7 view execution log +Click the “执行日志” button on the right side of the record,you can go to the execution log page,you can view the full execution log of the logic business code, shown as below: + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tvGI.png "在这里输入图片标题") + +### 4.8 stop running tasks +Just fit to running tasks,on the task log list page,click “终止任务” button on the right side of the record, it will send stop command to the executor where the task was executed,finally the task was killed and the task instance execute queue of this task will be clear. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_hIci.png "在这里输入图片标题") + +It is implemented by interrupt execute thread, it will trigger InterruptedException.so if JobHandler catch this execuption and handle this exception this function is unavailable. + +So if you want stop the running task ,the JobHandler need to handle InterruptedException separately by throw this exception.the right logic is as shown below: +``` +try{ + // do something +} catch (Exception e) { + if (e instanceof InterruptedException) { + throw e; + } + logger.warn("{}", e); +} +``` + +If JobHandler start child thread,child thread also must not catch InterruptedException,and it should throw exception. + + +### 4.9 delete execution log +On the task log list page, after you select executor and task, you can click"删除" button on the right side and it will pop-up "日志清理" window,on the pop-up window you can choose different log delete policy,choose the policy you want to execute and click "确定" button it will delele relative logs: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Ypik.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_EB65.png "在这里输入图片标题") + +### 4.10 delete task +Click the delete button on the right side of the task,the task will be deteted. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z9Qr.png "在这里输入图片标题") + +## 5. Overall design +### 5.1 Source directory introduction + - /doc :documentation and material + - /db :db scripts + - /xxl-job-admin :schedule and admin center + - /xxl-job-core :common core Jar + - /xxl-job-executor-samples :executor,Demo project(you can develop on this demo project or adjust your own exist project to executor project) + +### 5.2 configure database +XXL-JOB schedule module is implemented based on Quartz cluster,it’s “database” is extended based on Quartz’s 11 mysql tables. + +XXL-JOB custom Quartz table structure prefix(XXL_JOB_QRTZ_). + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_bNwm.png "在这里输入图片标题") + +The added tables as shown below: + - XXL_JOB_QRTZ_TRIGGER_GROUP:executor basic table, maintain the info about the executor; + - XXL_JOB_QRTZ_TRIGGER_REGISTRY:executor register table, maintain addressed of online executors and schedule center machines. + - XXL_JOB_QRTZ_TRIGGER_INFO:schedule extend table,it is used to save XXL-JOB schedule extended info,such as task group,task name,machine address,executor,input params of task and alarm email and so on. + - XXL_JOB_QRTZ_TRIGGER_LOG:schedule log table,it is used to save XXL-JOB task’s histry schedule info,such as :schedule result,execution result,input param of scheduled task,scheduled machine and executor and so on. + - XXL_JOB_QRTZ_TRIGGER_LOGGLUE:schedule log table,it is used to save XXL-JOB task’s histry schedule info,such as :schedule result,execution result,input param of scheduled task,scheduled machine and executor and so on. + +So XXL-JOB database total has 16 tables. + +### 5.3 Architecture design +#### 5.3.1 Design target +All schedule behavior has been abstracted into “schedule center” common platform , it dosen’t include business logic and just responsible for starting schedule requests. + +All tasks was abstracted into separate JobHandler and was managed by executors, executor is responsible for receiving schedule request and execute the relative JobHandler business. + +So schedule and task can be decoupled from each other, by the way it can improve the overall stability and scalability of the system. + +#### 5.3.2 System composition +- **Schedule module(schedule center)**: + it is responsible for manage schedule info,send schedule request accord task configuration and it is not include an business code.schedule system decouple with the task, improve the overall stability and scalability of the system, at the same time schedule system performance is no longer limited to task modules. + Support visualization, simple and dynamic management schedule information, include create,update,delete, GLUE develop and task alarm and so on, All of the above operations will take effect in real time,support monitor schedule result and execution log and executor failover. +- **Executor module(Executor)**: + it is responsible for receive schedule request and execute task logic,task module focuses on the execution of the task, Development and maintenance is simpler and more efficient. + Receive execution request, end request and log request from schedule center. + +#### 5.3.3 Architecture diagram + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png "在这里输入图片标题") + +### 5.4 Schedule module analysis +#### 5.4.1 Disadvantage of quartz +Quartz is a good open source project and was often as the first choice for job schedule.Tasks was managed by api in quartz cluster so it can avoid some disadvantages of single quartz instance,but it also has some disadvantage as shown below: + - problem 1:it is not humane while operate task by call apill. + - problem 2:it is need to store business QuartzJobBean into database, System Invasion is quite serious. + - problem 3:schedule logic and couple with QuartzJobBean in the same project,it will lead a problem in case that if schedule tasks gradually increased and task logic gradually increased,under this situation the performance of the schedule system will be greatly limited by business. +XXL-JOB solve above problems of quartz. + +#### 5.4.2 RemoteHttpJobBean +Under Quartz develop,task logic often was maintained by QuartzJobBean, couple is very serious.in XXL-JOB"Schedule module" and "task module" are completely decoupled,all scheduled tasks in schedule module use the same QuartzJobBean called RemoteHttpJobBean.the params of the tasks was maintained in the extended tables,when trigger RemoteHttpJobBean,it will parse different params and start remote cal l and it wil call relative remote executor. + +This call module is like RPC,RemoteHttpJobBean provide call proxy functionality,the executor is provided as remote service. + +#### 5.4.3 Schedule Center HA(Cluster) +It is based on Quartz cluster,databse use Mysql;while QUARTZ task schedule is used in Clustered Distributed Concurrent Environment,all nodes will report task info and store into database.it will fetch trigger from database while execute task,if trigger name and execute time is the same only one node will execute the task. + +``` +# for cluster +org.quartz.jobStore.tablePrefix = XXL_JOB_QRTZ_ +org.quartz.scheduler.instanceId: AUTO +org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX +org.quartz.jobStore.isClustered: true +org.quartz.jobStore.clusterCheckinInterval: 1000 +``` + +#### 5.4.4 Schedule threadpool +Default threads in the threadpool is 10 so it can avoid task schedule delay because of single thread block. + +``` +org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool +org.quartz.threadPool.threadCount: 10 +org.quartz.threadPool.threadPriority: 5 +org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true +``` + +business logic was executed on remote executor in XXL-JOB,schedule center just start one schedule request at every schedule time,executor will inqueue the request and response schedule center immediately. There is a huge difference from run business logic in quartz’s QuartzJobBean directly,just as Elephants and feathers; + +the logic of task in XXL-JOB schedule center is very light and single job average run time alaways under 100ms,(most is network time consume).so it can use limited threads to support a large mount of job run concurrently, 10 threads configured above can support at least 100 JOB normal execution. + +#### 5.4.5 @DisallowConcurrentExecution +This annotation is not used default by the schedule center of XXL-JOB schedule module, it use concurrent policy default,because RemoteHttpJobBean is common QuartzJobBean,so it greatly improve the capacity of schedule system and decrease the blocked chance of schedule module in the case of multi-threaded schedule. + +Every schedule module was scheduled and executed parallel in XXL-JOB,but tasks in executor is executed serially and support stop task. + +#### 5.4.6 misfire +The handle policy when miss the job’s trigger time. +he reason may be:restart service,schedule thread was blocked by QuartzJobBean, threads was exhausted,some task enable @DisallowConcurrentExecution,the last schedule was blocked and next schedule was missed. + +The default value of misfire in quartz.properties as shown below, unit in milliseconds: +``` +org.quartz.jobStore.misfireThreshold: 60000 +``` + +Misfire rule: + withMisfireHandlingInstructionDoNothing:does not trigger execute immediately and wait for next time schedule. + withMisfireHandlingInstructionIgnoreMisfires:execute immediately at the first frequency of the missed time. + withMisfireHandlingInstructionFireAndProceed:trigger task execution immediately at the frequency of the current time. + +XXL-JOB’s default misfire rule:withMisfireHandlingInstructionDoNothing + +``` +CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getJobCron()).withMisfireHandlingInstructionDoNothing(); +CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); +``` + +#### 5.4.7 log callback service +When schedule center of the schedule module was deployed as web service, on one side it play as schedule center, on the other side it also provide api service for executor. + +The source code location of schedule center’s “log callback api service” as shown below: +``` +xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback +``` + +Executor will execute task when it receive task execute request.it will notify the task execute result to schedule center when the task is done. + +#### 5.4.8 task HA(Failover) +If executor project was deployed as cluster schedule center will known all online executor nodes,such as:“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”. + +When "路由策略" select "故障转移(FAILOVER)",it will send heart beat check request in order while schedule center start schedule request. The first alive checked executor node will be selected and send schedule request to it. + +“调度备注” can be viewed on the monitor page when schedule success. As shown below: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jrdI.png "在这里输入图片标题") + +“调度备注” will display local schedule route path、executor’s "注册方式"、"地址列表" and task’s "路由策略"。Under "故障转移(FAILOVER)" policy, schedule center take first address to do heartbeat detection, heat beat fail will automatically skip, the second address heart beat fail…… until the third address “127.0.0.1:9999” heart beat success, it was selected as target executor, then send schedule request to target executor, now the schedule process is end wait for the executor’s callback execution result. + +#### 5.4.9 schedule log +Every time when task was scheduled in the schedule center it will record a task log, the task log include three part as shown below: + +- 任务信息:include executor address、JobHandler and executor params,accord these parameters it can locate specific machine and task code that the task will be executed. +- 调度信息:include schedule time、schedule result and schedule log and so on,accord these parameters you can understand some task schedule info of schedule center. +- 执行信息:include execute time、execute result and execute log and so on, accord these parameters you can understand the task execution info in the executor. + +Schedule log stands fo single task schedule, attribute description is as follows: +- 执行器地址:machine addresses on which task will be executed. +- JobHandler:JobHandler name of task under Bean module. +- 任务参数:the input parameters of task +- 调度时间:the schedule time started by schedule center. +- 调度结果:schedule result of schedule center,SUCCESS or FAIL. +- 调度备注:remark info of task scheduled by schedule center, such as address heart beat log. +- 执行时间:the callback time when the task is done in the executor. +- 执行结果:task execute result in the executor,SUCCESS or FAIL. +- 执行备注:task execute remark info in the executor,such as exception log. +- 执行日志:full execution log of the business code during execution of the task,go and see “4.7 view execution log”. + +#### 5.4.10 Task dependency +principle:every task has a task key in XXL-JOB, every task can configure property “child task Key”,it can match task dependency relationship through task key. + +When parent task end execute and success, it will match child task dependency accord child task key, it will trigger child task execute once if it matched child task. + +On the task log page ,you can see matched child task and triggered child task’s log info when you “查看”button of “执行备注”,otherwise the child task didin’t execute, as shown beleow: + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Wb2o.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jOAU.png "在这里输入图片标题") + +### 5.5 Task "run mode" analysis +#### 5.5.1 "Bean模式" task +Development steps:go and see "chapter 3" . +principle: every Bean mode task is a Spring Bean instance and it is maintained in executor project’s Spring container. task class nedd to add “@JobHandler(value="name")” annotation, because executor identify task bean instance in spring container through annotation. Task class nedd to implements interface IJobHandler, task logic code in method execute(), the task logic in execute() method will be executed when executor received a schedule request from schedule center. + +#### 5.5.2 "GLUE模式(Java)" task +Development steps:go and see "chapter 3" . +Principle : every "GLUE模式(Java)" task code is a class implemets interface IJobHandler, when executor received schedule request from schedule center these code will be loaded by Groovy classloader and instantiate into a Java object and inject spring bean service declared in this code at the same time(please confirm service and class reference in Glue code exist in executor project), then call the object’s execute() method and execute task logic. + +#### 5.5.3 GLUE模式(Shell) + GLUE模式(Python) +Development steps:go and see "chapter 3" . +principle:the source code of script task is maintained in schedule center and script logic will be executed in executor. when script task was triggered, executor will load script source code and generate a script file on the machine where executor was deployed, the script will be called by java code, the script output log will be written to the task log file in real time so that we can monitor script execution in real time through schedule center, the return code 0 stands for success other for fail. + +All supported types of scripts as shown beloes: + + - shell script:shell script task will be enabled when select "GLUE模式(Shell)"as task run mode. + - python script: python script task will be enabled when select " GLUE模式(Python)"as task run mode. + + +#### 5.5.4 executor +Executor is actually an embedded Jetty server with default port 9999, as shown below(parameter:xxl.job.executor.port). + +Executor will identify Bean mode task in spring container through @JobHandler When project start, it will be managed use the value of annotation as key. + +When executor received schedule request from schedule center, if task type is “Bean模式” it will match bean mode task in Spring container and call it’s execute() method and execute task logic. if task type is “GLUE模式”, it will load Glue code, instantiate a Java object and inject other spring service(notice: the spring service injected in Glue code must exist in the same executor project), then call execute() method and execute task logic. + +#### 5.5.5 task log +XXL-JOB will generate a log file for every schedule request, the log info will be recorded by XxlJobLogger.log() method, the log file will be loaded when view log info through schedule center. + +(history version is implemented by overriding LOG4J’s Appender so it exists dependency restrictions, The way has been discraded in the new version) + +The location of log file can be specified in executor configuration file, default pattern is : /data/applogs/xxl-job/jobhandler/formatted date/primary key for database scheduling log records.log”. + +When start child thread in JobHandler, child thread will print log in parent JobHandler thread’s execute log in order to trace execute log. + +### 5.6 Communication module analysis + +#### 5.6.1 A complete task schedule communication process + - 1,schedule center send http request to executor, and the service in executor in fact is a jetty server with default port 9999. + - 2,executor execute task logic. + - 3,executor http callback with schedule center for schedule result, the service in schedule center used to receive callback request from executor is a set of api opended to executor. + +#### 5.6.2 Encrypt Communication data +When scheduler center send request to executor, it will use RequestModel and ResponseModel object to encapsulate schedule request parameters and response data, these two object will be serialized before communication, data protocol and time stamp will be checked so that achieve data encryption target. + +### 5.7 task register and task auto discover +Task executor machine property has been canceled from v1.5, instead of task register and auto discovery, get remote machine address dynamic. + + AppName: unique identify of executor cluster, executor is minimal unite of task register, every task recognize machine addresses under the executor on which it was binded. + Beat: heartbeat cycle of task register, default is 15s, and the time executor usedto register is twice the time, the time used to auto task discover is twice the beat time, the invalid time of register is twice the Beat time. + registry table: see XXL_JOB_QRTZ_TRIGGER_REGISTRY table, it will maintain a register record periodically while task register, such as the bind relationship between machine address and AppName, so that schedule center can recognize machine list by AppName dynamicly. + +To ensure system lightweight and reduce learning costs, it did not use Zookeeper as register center, Use DB as register center to do task registration. + +### 5.8 task execute result +Since v1.6.2, the task execute result is recognized through ReturnT of IJobHandler, it executes success when return value meets the condition "ReturnT.code == ReturnT.SUCCESS_CODE" , or it executes fail, and it can callback error message info to schedule center through ReturnT.msg, so it can control task execute results in the task logic. + +### 5.9 slice broadcat & dynamic slice +When “分片广播” is selected as route policy in executor cluster, one task schedule will broadcast all executor node in cluster to trigger task execute in every executor, pass slice parameter at the same time, so we can develop slice task by slice parameters. + +"分片广播" break the task by the dimensions of executor, support dynamic extend executor cluster so that it can add slice number dynamically to do business process, In case of large amount of data process can significantly improve task processing capacity and speed. + +The develop process of "分片广播" is the same as general task, The difference is that you can get slice parameters,code as shown below(go and see ShardingJobHandler in execuotr example ): + + int shardIndex = XxlJobContext.getXxlJobContext().getShardIndex(); + int shardTotal = XxlJobContext.getXxlJobContext().getShardTotal(); + +This slice parameter object has two properties: + + index:the current slice number(start with 0),stands for the number of current executor in the executor cluster. + total:total slice number,stands for total slices in the executor cluster. + +This feature applies to scenes as shown below: +- 1、slice task scene:when 10 executor to handle 10w records, 1w records need to be handled per machine, time-consuming 10 times lower; +- 2、Broadcast task scene:broadcast all cluster nodes to execute shell script、broadcast all cluster nodes to update cache. + +### 5.10 AccessToken +To improve system security it is need to check security between schedule center and executor, just allow communication between them when AccessToken of each other matched. + +The AccessToken of scheduler center and executor can be configured by xxl.job.accessToken. + +There are only two settings when communication between scheduler center and executor just: + +- one:do not configure AccessToken on both, close security check. +- two:configure the same AccessToken on both; + +### 5.11 Dispatching center API services +The scheduling center provides API services for executors and business parties to choose to use, and the currently available API services are available. + + 1. Job result callback service; + 2. Executor registration service; + 3. Executor registration remove services; + 4. Triggers a single execution service, and support the task to be triggered according to the business event; + +The scheduling center API service location: com.xxl.job.core.biz.AdminBiz.java + +The scheduling center API service requests reference code:com.xxl.job.adminbiz.AdminBizTest.java + + +## 6 Version update log +### 6.1 version V1.1.x,New features [2015-12-05] +**【since V1.1.x,XXL-JOB was used by company hiring me,alias Ferrari inner company,the latest version is recommended for new project】** +- 1、simple:support CRUD operation through Web page, simple and one minute to get started; +- 2、dynamic:support dynamic update task status,pause/recover task and effective in real time; +- 3、service HA:task info stored in mysql, Job service support cluster to make sure service HA; +- 4、task HA:when some Job services hangs up, tasks will be assigned to some other alive machines, if all nodes of the cluster hangs up, it will compensate for the execution of lost task when restart; +- 5、one task instance will only be executed on one executor; +- 6、task is executed serially; +- 7、support for custom parameters; +- 8、Support pause task execution remotely . + +### 6.2 version V1.2.x,New features [2016-01-17] +- 1、support task group; +- 2、suport local task, remote task; +- 3、support two types underlying communication ,Servlet or JETTY; +- 4、support task log; +- 5、support serially execution,parallel execution; + + Description:system architecture of V1.2 divided by function as shown below: + + - schedule module(schedule center):Responsible for managing schedule information,send schedule request according to the schedule configuration; + - execute module(executor):Responsible for receiving schedule request and execute task logic; + - communication module:Responsible for the communication between the schedule module and execute module; + advantage: + + - Decouple:execute module supply task api, schedule module maintains schedule information, The business is independent of each other; + - High scalability; + - stability; + +### 6.3 version V1.3.0,New features [2016-05-19] +- 1、discard local task module, remote task was recommended, easy to decouple system, the JobHandler of task was called executor. +- 2、dicard underlying communication type servlet, JETTY was recommended, schedule and callback bidirectional communication, rebuild the communication logic; +- 3、UI interactive optimization:optimize left menu expansion and menu item selected status , task list opens the table with compression optimization; +- 4、【important】executor is subdivided into two develop mode:BEAN、GLUE: + + Introduction to the executor mode: + - BEAN mode executor:every executor is a Spring Bean instance,it was recognized and scheduled by XXL-JOB through @JobHandler annotation; + -GLUE mode executor:every executor corresponds to a piece of code,edited and maintained online by Web, Dynamic compile and takes effect in real time, executor is responsible for loading GLUE code and executing; + +### 6.4 version V1.3.1,New features [2016-05-23] +- 1、Update project directory structure: + - /xxl-job-admin -------------------- 【schedule center】:Responsible for managing schedule information,send schedule request according to schedule configuration; + - /xxl-job-core ----------------------- Public core dependence + - /xxl-job-executor-example ------ 【executor】:Responsible for receiving scheduling request and execute task logic; + - /db ---------------------------------- create table script + - /doc --------------------------------- user manual +- 2、Upgrade the user manual under the new directory structure; +- 3、Optimize some interactions and UI; + +### 6.5 version V1.3.2,New features [2016-05-28] +- 1、Schedule logic for transactional handle; +- 2、executor asynchronous callback execution log; +- 3、【important】based on HA support of schedule center,extend executor’s Failover support,Support configure multiple execution addresses; + +### 6.6 version V1.4.0 New features [2016-07-24] +- 1、Task dependency: it is implemented by trigger event, it will automatically trigger a child task schedule after Task execute success and callback, multiple child tasks are separated by commas; +- 2、executor source code has been reconstructed, optimize underlying db script; +- 3、optimize task thread group logic of executor, before it is group by executor’s JobHandler so when multiple task reuse Jobhanlder will cause block with each other. Now it is grouped by task of schedule center so tasks are isolated from task execution. +- 4、optimize communication scheme between executor and schedule center, a simple RPC protocol was implemented through Hex + HC, optimize the maintenance and analysis process of communication parameters. +- 5、schedule center, create/edit task, page attribute adjustment: + - 5.1、the property JobName was removed from task add/edit page and it is changed to automatically generate by system: this field before is used to identify a task in schedule center and did not use in other scenes, so remove it to simplify the task creation; + - 5.2、adjust "GLUE模式" property in task add/edit page to near JobHandler input box; + - 5.3、"报警阈值" property was removed from task add/edit page; + - 5.4、"子任务Key" property was removed from task add/edit page, the key of task can be acquired from task list page, child task will be triggered by child task key when main task execute success. +- 6、bug fix: + - 6.1、optimize jetty executor shutdown, solve one problem may cause jetty could not shutdown. + - 6.2、optimize callback of executor task queue when task execute finish. Solve a problem which may cause task could not callback. + - 6.3、Optimize Page List Parameters of Schedule Center, solve one problem which may be caused by post length limit of server. + - 6.4、optmize executor Jobhandler annotation, solve a problem that container could not load the JobHandler caused by the transaction proxy. + - 6.5、optimize remote schedule, disable retry policy, solve a problem may caused repeat call; + +Tips: V1.3.x release has been published , enter the maintenance phase, branch address is [V1.3](https://github.com/xuxueli/xxl-job/tree/v1.3) .New features will be updated continuously in the master branch. + +### 6.7 version V1.4.1 New features [2016-09-06] +- 1、project successfully pushed to maven central warehouse, Central warehouse address and dependency as shown below: + ``` + <!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ --> + <dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>${最新稳定版}</version> + </dependency> + ``` +- 2、To adapt to the rules of central warehouse, groupId has been changed from com.xxl to com.xuxueli. +- 3、to resolve the problem that sub-modules can not be compiled separately, system version is not maintained in the project root pom, each sub-module is configured separately for version configuration; +- 4、optimize data byte length statistics rule of RPC communication it may reduce 50% of data traffic; +- 5、IJobHandler cancel task return value, before the execution status is judged by the return value, now it instead of task was executed successfully by default only when exception was caught the task execution was judged failed. +- 6、optimize system public pop-up box as a plugin; +- 7、optimize table structure and the table name now is upper case; +- 8、modify ContentType of JSON response from exception handler of schedule center to fix the bug that it is could not recognized by browser. + +### 6.8 version V1.4.2 New features [2016-09-29] +- 1、push V1.4.2 to maven central warehouse, main version V1.4 enter maintenance phase; +- 2、fix problem task list offset when add task; +- 3、fix a style disorder problem that caused by bootstrap does not support the modal frame overlap , the problem occurs when the task is edited; +- 4、optimize schedule status when schedule timeout and Handler could not matched; +- 5、the task could not stop problem caused by catch exception has given solution; + +### 6.9 version V1.5.0 New features [2016-11-13] +- 1、task register: executor registers the task automatically, schedule center will automatically discover the registered task and trigger execution. +- 2、add parameter AppName for executor: AppName is the unique identifier of each executor cluster, register periodically and automatically with AppName. +- 3、add column executor management in schedule center : manage online executors, automatically discover registered executors via the property AppName。Only managed executors are allowed to be used; +- 4、change Task group attribute to executor : each task needs to be bound to the specified exector, schedule address is obtained by binded executor; +- 5、discard property task machine: by the way of binding task with executor, automatically discovers registered remote executor address and triggers schedule request. +- 6、add DBGlueLoader in public dependency, it implement GLUE source code calssloader based on native jdbc, Reduce third party reliance (mybatis,spring-orm etc); simplify and optimize executor configuration (for GLUE task), Reduce the difficulty of getting started; +- 7、adjust table structure, reconstruct the project; +- 8、schedule center automatically registered and found, failover: schedule center periodically registered automatically, task callback can recognize all online schedule center addresses, task callback support failover so that it can avoid single point of risk. + +### 6.10 version V1.5.1 New features [2016-11-13] +- 1、Reconstruct the underlying code and optimize logic, clean POM and Clean Code; +- 2、Servlet/JSP Spec selected 3.0/2.2; +- 3、Spring updated to 3.2.17.RELEASE version; +- 4、Jetty updated to version 8.2.0.v20160908; +- 5、has push V1.5.0 and V1.5.1 to maven central warehouse; + +### 6.10 version V1.5.2 New features [2017-02-28] +- 1、optimize IP tools class which used to gets IP address,IP static cache; +- 2、both executor and schedule center support customize registered IP address;Solve problem when machine has multiple network card and get the wrong card; +- 3、solve the problem that it will generate multiple log files when executed across days; +- 4、the non-sensitive log level is adjusted to debug; +- 5、Upgrade the database connection pool to c3p0; +- 6、optimize log4j property of executor,remove invalid attribute; +- 7、reconstruct underlying code and optimize logic and Clean Code; +- 8、optimize Dependency Injection Logic of GLUE, support injected as alias; + +### 6.11 version V1.6.0 New features [2017-03-13] +- 1、upgrade communication scheme,the HEX communication model is adjusted to the B-RPC model based on HTTP; +- 2、executor supports set execution address list manually,provide switch to use automatically registered address or manually set address; +- 3、executor route rules:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移; +- 4、unified thread model and thread destruction scheme (by the way of listener or stop() method,Destroy the thread when container is destroyed;Daemon is sometimes not ideal); +- 5、unified system configuration data,Unified managed by configuration files; +- 6、CleanCode,Clean up invalid historical parameters; +- 7、extend data structure and adjust related table structure; +- 8、new created task defaults to a non-running state; +- 9、optimize update logic of GLUE mode task instance , The original update is based on the timeout value and now is updated according to the version number,version number plus one while source changed; + +### 6.12 version V1.6.1 New features [2017-03-25] +- 1、Rolling log; +- 2、reconstruct WebIDE interactive; +- 3、enhanced communication check,filter unnormal requests effectively; +- 4、enhanced permission check,Using dynamic login TOKEN(recommend instead of internal SSO); +- 5、optimize database configuration,solve garbled problem; + +### 6.13 version V1.6.2 New features [2017-04-25] +- 1、execution report:support view run time data in real time, such as task number, total schedule number, executor number etc., include schedule report , such as scheduled distribution graph on date, scheduled success distribution graph etc; +- 2、JobHandler support set return value for tasks, it is easy to control task execute result in task logic; +- 3、the problem could not view exception info when resource path include space or chinese word casused resource file could not be loaded; +- 4、optimize route policy:fix problems that Loop and LFU routing policy counters are no limit and first route is focused on the first machine; + +### 6.14 version V1.7.0 New features [2017-05-02] +- 1、script task:support develop and run script task by GLUE, include script type such as Shell、Python and Groovy; +- 2、add spring-boot type executor example project; +- 3、upgrade jetty to version 9.2; +- 4、task execute log remove log4j dependency, instead of self-realization,Thus eliminate the dependency on the log component; +- 5、executor remove GlueLoader dependency,instead of push mode,thus GLUE source code load no longer rely on JDBC; +- 6、get the project name when login and redirect, solve 404 problem when it is not deployed by the directory; + +### 6.15 version V1.7.1 New features [2017-05-08] +- 1、unified write and read code of execute log as UTF-8,solve log garbled problem under windows environment; +- 2、communication timeout period is limited to 10s,To avoid schedule thread is occupied under abnormal situation; +- 3、adjust executor , server stat, destroy and register logic; +- 4、optimize Jetty Server shutdown logic, repair port occupation caused by executor could not be closed normally and frequent printe c3p0 log probleam; +- 5、start child thread in JobHandler,support child thread print execute log and view by Rolling; +- 6、task log cleanup; +- 7、pop-up component is replaced by layer; +- 8、upgrade quartz to version 2.3.0; + +### 6.16 version V1.7.2 New features [2017-05-17] +- 1、block handle policy:the policy when schedule is too frequently and the executor it too late to handle, include multiple strategies:single machine serially execute(default)、discard subsequent schedule、override before schedule; +- 2、fail handle policy:handle policy when scheduled fail, include :failure alarm(default)、failed to retry; +- 3、The communication timeout is adjusted to 180s; +- 4、executor and database are completely decoupled,But the executor needs to configure schedule center cluster address。schedule center provides APIs for executor callbacks and heartbeat registration services,cancel jetty inner schedule center, heartbeat cycle is adjusted to 30s,heartbeat failure is triple heartbeat; +- 5、fix executor parameters lost bug when edit; +- 6、add task test Demo to make task logic test easier; + +### 6.17 version V1.8.0 New features [2017-07-17] +- 1、optimize update logic of task Cron,instead of rescheduleJob,at the same time preventing set cron repeatedly; +- 2、optimize API callback service failed status code,facilitate troubleshooting; +- 3、XxlJobLogger support multi-parameter; +- 4、route policy add "忙碌转移" mode:Perform idle detection in sequence,The first idle test successfully machine is selected as the target executor and trigger schedule; +- 5、reconstruct route policy code; +- 6、fix executor repeat registration problem; +- 7、Task thread will be destroyed after 30 times idle turn, reduce the inefficient thread consumption of low frequency tasks; +- 8、Executor task execution result batch callback so that reduce callback frequency to improve actuator performance; +- 9、cancle XML configuration of springboot executor project,instead of class configuration; +- 10、supports filter execute log based on running status; +- 11、optimize scheduling Center Task Registration Detection Logic; + +### 6.18 version V1.8.1 New features [2017-07-30] +- 1、slice broadcast task:When slice broadcast is selected as route policy in executor cluster, one task schedule will broadcast all executor node in cluster to trigger task execute in every executor, pass slice parameter at the same time, so we can develop slice task by slice parameters; +- 2、dynamic slice: break the task by the dimensions of executor, support dynamic extend executor cluster so that it can add slice number dynamically to do business process, In case of large amount of data process can significantly improve task processing capacity and speed; +- 3、executor JobHandler disables name conflicts; +- 4、executor cluster address list for natural sorting; +- 5、add test cases and optimize DAO layer code for Scheduling center; +- 6、schedule Center API service change to self-study RPC framework to u nify communication model; +- 7、add schedule center API service test Demo, convenient in dispatch center API extension and testing; +- 8、Task list page interaction optimization,The task list is automatically refreshed when the executor group is replaced,create new job defaults to locate current executor position; +- 9、access Token:To improve system security,it is used for safety check between schedule center and executor, communication allowed just when Both Access Token matched; +- 10、upgrade springboot version to 1.5.6.RELEASE of executor; +- 11、unify maven version dependency management; + +### 6.19 version V1.8.2 New features[Coding] +- 1,support configuring the HTTPS for executor callback URL; +- 2,Standardize project directory for extend multi executors; +- 3,add JFinal type executor sample project; + +### TODO LIST +- 1,Task privilege management:control privilege on executor, check privilege on core operations; +- 2,Task slice routing:using consistent Hash algorithm to calculate slice order as stable as possible, even if there is fluctuation in the registration machine will not cause large fluctuations in the order of slice. Currently using IP natural sorting can meet the demand,to be determined; +- 3,Failure retry optimization:The current failure to retry logic is execute the request logic once again after the scheduled request fails。The optimization point is retry for both scheduling and execution failures, retry a full schedule when retrying,This may lead schedule failure to an infinite loop,to be determined; +- 4,write file when callback failed,read the log when viewing the log,callback confirm after rebooting; +- 5,Task dependency,flow chart,child task + aggregation task,log of each node; +- 6,Scheduled task priority; +- 7,Remove quartz dependencies and rewrite scheduld module:insert the next execution record into delayqueue when add or resume task, schedule center cluster compete distributed lock,successful nodes bulk load expired delayqueue data and batch execution; +- 8,springboot and docker image,and push docker image to the central warehouse,further realize product out of the box; +- 9,globalization:schedule center interface and Official documents,add English version; +- 10,executor removal:notify schedule center and remove the corresponding execute node when executor is destroyed, improve the timeliness of executor state recognized; + +## 7. Other + +### 7.1 Contributing +Contributions are welcome! Open a pull request to fix a bug, or open an [Issue](https://github.com/xuxueli/xxl-job/issues/) to discuss a new feature or change. + +### 7.2 used records(record just for spread,Product is open source and free of charge) +Record for spread product and product is free and open source. +Welcome to [check in](https://github.com/xuxueli/xxl-job/issues/1 )on github. + +### 7.3 Copyright and License +This product is open source and free, and will continue to provide free community technical support. Individual or enterprise users are free to access and use. + +- Licensed under the GNU General Public License (GPL) v3. +- Copyright (c) 2015-present, xuxueli. + +--- +### Donate +No matter how much the amount is enough to express your thought, thank you very much :) [To donate](https://www.xuxueli.com/page/donate.html ) diff --git "a/xxl-job/doc/XXL-JOB\345\256\230\346\226\271\346\226\207\346\241\243.md" "b/xxl-job/doc/XXL-JOB\345\256\230\346\226\271\346\226\207\346\241\243.md" new file mode 100644 index 0000000..0a04363 --- /dev/null +++ "b/xxl-job/doc/XXL-JOB\345\256\230\346\226\271\346\226\207\346\241\243.md" @@ -0,0 +1,2324 @@ +## 《分布式任务调度平台XXL-JOB》 + +[![Actions Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/) +[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-job.svg)](https://github.com/xuxueli/xxl-job/releases) +[![GitHub stars](https://img.shields.io/github/stars/xuxueli/xxl-job)](https://github.com/xuxueli/xxl-job/) +[![Docker Status](https://img.shields.io/docker/pulls/xuxueli/xxl-job-admin)](https://hub.docker.com/r/xuxueli/xxl-job-admin/) +[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html) +[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat)](https://www.xuxueli.com/page/donate.html) + +[TOCM] + +[TOC] + +## 一、简介 + +### 1.1 概述 +XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 + +### 1.2 社区交流 +- [社区交流](https://www.xuxueli.com/page/community.html) + +### 1.3 特性 +- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; +- 2、动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效; +- 3、调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA; +- 4、执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA; +- 5、注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址; +- 6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务; +- 7、触发策略:提供丰富的任务触发策略,包括:Cron触发、固定间隔触发、固定延时触发、API(事件)触发、人工触发、父子任务触发; +- 8、调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等; +- 9、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; +- 10、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务; +- 11、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试; +- 12、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式; +- 13、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等; +- 14、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务; +- 15、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 +- 16、故障转移:任务路由策略选择"故障转移"情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。 +- 17、任务进度监控:支持实时监控任务进度; +- 18、Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志; +- 19、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。 +- 20、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS、PHP、PowerShell等类型脚本; +- 21、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +- 22、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔; +- 23、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行; +- 24、自定义任务参数:支持在线配置调度任务入参,即时生效; +- 25、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞; +- 26、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性; +- 27、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件; +- 28、推送maven中央仓库: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用; +- 29、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等; +- 30、全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行; +- 31、跨语言:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案; +- 32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文; +- 33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; +- 34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; +- 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; +- 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; + +### 1.4 发展 +于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计…… + +于2015-11月,XXL-JOB终于RELEASE了第一个大版本V1.0, 随后我将之发布到OSCHINA,XXL-JOB在OSCHINA上获得了@红薯的热门推荐,同期分别达到了OSCHINA的“热门动弹”排行第一和git.oschina的开源软件月热度排行第一,在此特别感谢红薯,感谢大家的关注和支持。 + +于2015-12月,我将XXL-JOB发表到我司内部知识库,并且得到内部同事认可。 + +于2016-01月,我司展开XXL-JOB的内部接入和定制工作,在此感谢袁某和尹某两位同事的贡献,同时也感谢内部其他给与关注与支持的同事。 + +于2017-05-13,在上海举办的 "[第62期开源中国源创会](https://www.oschina.net/event/2236961)" 的 "放码过来" 环节,我登台对XXL-JOB做了演讲,台下五百位在场观众反响热烈([图文回顾](https://www.oschina.net/question/2686220_2242120) )。 + +于2017-10-22,又拍云 Open Talk 联合 Spring Cloud 中国社区举办的 "[进击的微服务实战派上海站](https://opentalk.upyun.com/303.html)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。 + +于2017-12-11,XXL-JOB有幸参会《[InfoQ ArchSummit全球架构师峰会](http://bj2017.archsummit.com/)》,并被拍拍贷架构总监"杨波老师"在专题 "[微服务原理、基础架构和开源实践](http://bj2017.archsummit.com/training/2)" 中现场介绍。 + +于2017-12-18,XXL-JOB参与"[2017年度最受欢迎中国开源软件](http://www.oschina.net/project/top_cn_2017?sort=1)"评比,在当时已录入的约九千个国产开源项目中角逐,最终进入了前30强。 + +于2018-01-15,XXL-JOB参与"[2017码云最火开源项目](https://www.oschina.net/news/92438/2017-mayun-top-50)"评比,在当时已录入的约六千五百个码云项目中角逐,最终进去了前20强。 + +于2018-04-14,iTechPlus在上海举办的 "[2018互联网开发者大会](http://www.itdks.com/eventlist/detail/2065)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。 + +于2018-05-27,在上海举办的 "[第75期开源中国源创会](https://www.oschina.net/event/2278742)" 的 "架构" 主题专场,我登台进行“基础架构与中间件图谱”主题演讲,台下上千位在场观众反响热烈([图文回顾](https://www.oschina.net/question/3802184_2280606) )。 + +于2018-12-05,XXL-JOB参与"[2018年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2018?sort=1)"评比,在当时已录入的一万多个开源项目中角逐,最终排名第19名。 + +于2019-12-10,XXL-JOB参与"[2019年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2019)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第9名。 + +于2020-11-16,XXL-JOB参与"[2020年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2020)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第8名。 + +于2021-12-06,XXL-JOB参与"[2021年度OSC中国开源项目评选](https://www.oschina.net/project/top_cn_2021) "评比,在当时已录入的一万多个开源项目中角逐,最终当选"最受欢迎项目"。 + +> 我司大众点评目前已接入XXL-JOB,内部别名《Ferrari》(Ferrari基于XXL-JOB的V1.1版本定制而成,新接入应用推荐升级最新版本)。 +据最新统计, 自2016-01-21接入至2017-12-01期间,该系统已调度约100万次,表现优异。新接入应用推荐使用最新版本,因为经过数十个版本的更新,系统的任务模型、UI交互模型以及底层调度通讯模型都有了较大的优化和提升,核心功能更加稳定高效。 + +至今,XXL-JOB已接入多家公司的线上产品线,接入场景如电商业务,O2O业务和大数据作业等,截止最新统计时间为止,XXL-JOB已接入的公司包括不限于: + + - 1、大众点评【美团点评】 + - 2、山东学而网络科技有限公司; + - 3、安徽慧通互联科技有限公司; + - 4、人人聚财金服; + - 5、上海棠棣信息科技股份有限公司 + - 6、运满满【运满满】 + - 7、米其林 (中国区)【米其林】 + - 8、妈妈联盟 + - 9、九樱天下(北京)信息技术有限公司 + - 10、万普拉斯科技有限公司【一加手机】 + - 11、上海亿保健康管理有限公司 + - 12、海尔馨厨【海尔】 + - 13、河南大红包电子商务有限公司 + - 14、成都顺点科技有限公司 + - 15、深圳市怡亚通 + - 16、深圳麦亚信科技股份有限公司 + - 17、上海博莹科技信息技术有限公司 + - 18、中国平安科技有限公司【中国平安】 + - 19、杭州知时信息科技有限公司 + - 20、博莹科技(上海)有限公司 + - 21、成都依能股份有限责任公司 + - 22、湖南高阳通联信息技术有限公司 + - 23、深圳市邦德文化发展有限公司 + - 24、福建阿思可网络教育有限公司 + - 25、优信二手车【优信】 + - 26、上海悠游堂投资发展股份有限公司【悠游堂】 + - 27、北京粉笔蓝天科技有限公司 + - 28、中秀科技(无锡)有限公司 + - 29、武汉空心科技有限公司 + - 30、北京蚂蚁风暴科技有限公司 + - 31、四川互宜达科技有限公司 + - 32、钱包行云(北京)科技有限公司 + - 33、重庆欣才集团 + - 34、咪咕互动娱乐有限公司【中国移动】 + - 35、北京诺亦腾科技有限公司 + - 36、增长引擎(北京)信息技术有限公司 + - 37、北京英贝思科技有限公司 + - 38、刚泰集团 + - 39、深圳泰久信息系统股份有限公司 + - 40、随行付支付有限公司 + - 41、广州瀚农网络科技有限公司 + - 42、享点科技有限公司 + - 43、杭州比智科技有限公司 + - 44、圳临界线网络科技有限公司 + - 45、广州知识圈网络科技有限公司 + - 46、国誉商业上海有限公司 + - 47、海尔消费金融有限公司,嗨付、够花【海尔】 + - 48、广州巴图鲁信息科技有限公司 + - 49、深圳市鹏海运电子数据交换有限公司 + - 50、深圳市亚飞电子商务有限公司 + - 51、上海趣医网络有限公司 + - 52、聚金资本 + - 53、北京父母邦网络科技有限公司 + - 54、中山元赫软件科技有限公司 + - 55、中商惠民(北京)电子商务有限公司 + - 56、凯京集团 + - 57、华夏票联(北京)科技有限公司 + - 58、拍拍贷【拍拍贷】 + - 59、北京尚德机构在线教育有限公司 + - 60、任子行股份有限公司 + - 61、北京时态电子商务有限公司 + - 62、深圳卷皮网络科技有限公司 + - 63、北京安博通科技股份有限公司 + - 64、未来无线网 + - 65、厦门瓷禧网络有限公司 + - 66、北京递蓝科软件股份有限公司 + - 67、郑州创海软件科技公司 + - 68、北京国槐信息科技有限公司 + - 69、浪潮软件集团 + - 70、多立恒(北京)信息技术有限公司 + - 71、广州极迅客信息科技有限公司 + - 72、赫基(中国)集团股份有限公司 + - 73、海投汇 + - 74、上海润益创业孵化器管理股份有限公司 + - 75、汉纳森(厦门)数据股份有限公司 + - 76、安信信托 + - 77、岚儒财富 + - 78、捷道软件 + - 79、湖北享七网络科技有限公司 + - 80、湖南创发科技责任有限公司 + - 81、深圳小安时代互联网金融服务有限公司 + - 82、湖北享七网络科技有限公司 + - 83、钱包行云(北京)科技有限公司 + - 84、360金融【360】 + - 85、易企秀 + - 86、摩贝(上海)生物科技有限公司 + - 87、广东芯智慧科技有限公司 + - 88、联想集团【联想】 + - 89、怪兽充电 + - 90、行圆汽车 + - 91、深圳店店通科技邮箱公司 + - 92、京东【京东】 + - 93、米庄理财 + - 94、咖啡易融 + - 95、梧桐诚选 + - 96、恒大地产【恒大】 + - 97、昆明龙慧 + - 98、上海涩瑶软件 + - 99、易信【网易】 + - 100、铜板街 + - 101、杭州云若网络科技有限公司 + - 102、特百惠(中国)有限公司 + - 103、常山众卡运力供应链管理有限公司 + - 104、深圳立创电子商务有限公司 + - 105、杭州智诺科技股份有限公司 + - 106、北京云漾信息科技有限公司 + - 107、深圳市多银科技有限公司 + - 108、亲宝宝 + - 109、上海博卡软件科技有限公司 + - 110、智慧树在线教育平台 + - 111、米族金融 + - 112、北京辰森世纪 + - 113、云南滇医通 + - 114、广州市分领网络科技有限责任公司 + - 115、浙江微能科技有限公司 + - 116、上海馨飞电子商务有限公司 + - 117、上海宝尊电子商务有限公司 + - 118、直客通科技技术有限公司 + - 119、科度科技有限公司 + - 120、上海数慧系统技术有限公司 + - 121、我的医药网 + - 122、多粉平台 + - 123、铁甲二手机 + - 124、上海海新得数据技术有限公司 + - 125、深圳市珍爱网信息技术有限公司【珍爱网】 + - 126、小蜜蜂 + - 127、吉荣数科技 + - 128、上海恺域信息科技有限公司 + - 129、广州荔支网络有限公司【荔枝FM】 + - 130、杭州闪宝科技有限公司 + - 131、北京互联新网科技发展有限公司 + - 132、誉道科技 + - 133、山西兆盛房地产开发有限公司 + - 134、北京蓝睿通达科技有限公司 + - 135、月亮小屋(中国)有限公司【蓝月亮】 + - 136、青岛国瑞信息技术有限公司 + - 137、博雅云计算(北京)有限公司 + - 138、华泰证券香港子公司 + - 139、杭州东方通信软件技术有限公司 + - 140、武汉博晟安全技术股份有限公司 + - 141、深圳市六度人和科技有限公司 + - 142、杭州趣维科技有限公司(小影) + - 143、宁波单车侠之家科技有限公司【单车侠】 + - 144、丁丁云康信息科技(北京)有限公司 + - 145、云钱袋 + - 146、南京中兴力维 + - 147、上海矽昌通信技术有限公司 + - 148、深圳萨科科技 + - 149、中通服创立科技有限责任公司 + - 150、深圳市对庄科技有限公司 + - 151、上证所信息网络有限公司 + - 152、杭州火烧云科技有限公司【婚礼纪】 + - 153、天津青芒果科技有限公司【芒果头条】 + - 154、长飞光纤光缆股份有限公司 + - 155、世纪凯歌(北京)医疗科技有限公司 + - 156、浙江霖梓控股有限公司 + - 157、江西腾飞网络技术有限公司 + - 158、安迅物流有限公司 + - 159、肉联网 + - 160、北京北广梯影广告传媒有限公司 + - 161、上海数慧系统技术有限公司 + - 162、大志天成 + - 163、上海云鹊医 + - 164、上海云鹊医 + - 165、墨迹天气【墨迹天气】 + - 166、上海逸橙信息科技有限公司 + - 167、沅朋物联 + - 168、杭州恒生云融网络科技有限公司 + - 169、绿米联创 + - 170、重庆易宠科技有限公司 + - 171、安徽引航科技有限公司(乐职网) + - 172、上海数联医信企业发展有限公司 + - 173、良彬建材 + - 174、杭州求是同创网络科技有限公司 + - 175、荷马国际 + - 176、点雇网 + - 177、深圳市华星光电技术有限公司 + - 178、厦门神州鹰软件科技有限公司 + - 179、深圳市招商信诺人寿保险有限公司 + - 180、上海好屋网信息技术有限公司 + - 181、海信集团【海信】 + - 182、信凌可信息科技(上海)有限公司 + - 183、长春天成科技发展有限公司 + - 184、用友金融信息技术股份有限公司【用友】 + - 185、北京咖啡易融有限公司 + - 186、国投瑞银基金管理有限公司 + - 187、晋松(上海)网络信息技术有限公司 + - 188、深圳市随手科技有限公司【随手记】 + - 189、深圳水务科技有限公司 + - 190、易企秀【易企秀】 + - 191、北京磁云科技 + - 192、南京蜂泰互联网科技有限公司 + - 193、章鱼直播 + - 194、奖多多科技 + - 195、天津市神州商龙科技股份有限公司 + - 196、岩心科技 + - 197、车码科技(北京)有限公司 + - 198、贵阳市投资控股集团 + - 199、康旗股份 + - 200、龙腾出行 + - 201、杭州华量软件 + - 202、合肥顶岭医疗科技有限公司 + - 203、重庆表达式科技有限公司 + - 204、上海米道信息科技有限公司 + - 205、北京益友会科技有限公司 + - 206、北京融贯电子商务有限公司 + - 207、中国外汇交易中心 + - 208、中国外运股份有限公司 + - 209、中国上海晓圈教育科技有限公司 + - 210、普联软件股份有限公司 + - 211、北京科蓝软件股份有限公司 + - 212、江苏斯诺物联科技有限公司 + - 213、北京搜狐-狐友【搜狐】 + - 214、新大陆网商金融 + - 215、山东神码中税信息科技有限公司 + - 216、河南汇顺网络科技有限公司 + - 217、北京华夏思源科技发展有限公司 + - 218、上海东普信息科技有限公司 + - 219、上海鸣勃网络科技有限公司 + - 220、广东学苑教育发展有限公司 + - 221、深圳强时科技有限公司 + - 222、上海云砺信息科技有限公司 + - 223、重庆愉客行网络有限公司 + - 224、数云 + - 225、国家电网运检部 + - 226、杭州找趣 + - 227、浩鲸云计算科技股份有限公司 + - 228、科大讯飞【科大讯飞】 + - 229、杭州行装网络科技有限公司 + - 230、即有分期金融 + - 231、深圳法司德信息科技有限公司 + - 232、上海博复信息科技有限公司 + - 233、杭州云嘉云计算有限公司 + - 234、有家民宿(有家美宿) + - 235、北京赢销通软件技术有限公司 + - 236、浙江聚有财金融服务外包有限公司 + - 237、易族智汇(北京)科技有限公司 + - 238、合肥顶岭医疗科技开发有限公司 + - 239、车船宝(深圳)旭珩科技有限公司) + - 240、广州富力地产有限公司 + - 241、氢课(上海)教育科技有限公司 + - 242、武汉氪细胞网络技术有限公司 + - 243、杭州有云科技有限公司 + - 244、上海仙豆智能机器人有限公司 + - 245、拉卡拉支付股份有限公司【拉卡拉】 + - 246、虎彩印艺股份有限公司 + - 247、北京数微科技有限公司 + - 248、广东智瑞科技有限公司 + - 249、找钢网 + - 250、九机网 + - 251、杭州跑跑网络科技有限公司 + - 252、深圳未来云集 + - 253、杭州每日给力科技有限公司 + - 254、上海齐犇信息科技有限公司 + - 255、滴滴出行【滴滴】 + - 256、合肥云诊信息科技有限公司 + - 257、云知声智能科技股份有限公司 + - 258、南京坦道科技有限公司 + - 259、爱乐优(二手平台) + - 260、猫眼电影(私有化部署)【猫眼电影】 + - 261、美团大象(私有化部署)【美团大象】 + - 262、作业帮教育科技(北京)有限公司【作业帮】 + - 263、北京小年糕互联网技术有限公司 + - 264、山东矩阵软件工程股份有限公司 + - 265、陕西国驿软件科技有限公司 + - 266、君开信息科技 + - 267、村鸟网络科技有限责任公司 + - 268、云南国际信托有限公司 + - 269、金智教育 + - 270、珠海市筑巢科技有限公司 + - 271、上海百胜软件股份有限公司 + - 272、深圳市科盾科技有限公司 + - 273、哈啰出行【哈啰】 + - 274、途虎养车【途虎】 + - 275、卡思优派人力资源集团 + - 276、南京观为智慧软件科技有限公司 + - 277、杭州城市大脑科技有限公司 + - 278、猿辅导【猿辅导】 + - 279、洛阳健创网络科技有限公司 + - 280、魔力耳朵 + - 281、亿阳信通 + - 282、上海招鲤科技有限公司 + - 283、四川商旅无忧科技服务有限公司 + - 284、UU跑腿 + - 285、北京老虎证券【老虎证券】 + - 286、悠活省吧(北京)网络科技有限公司 + - 287、F5未来商店 + - 288、深圳环阳通信息技术有限公司 + - 289、遠傳電信 + - 290、作业帮(北京)教育科技有限公司【作业帮】 + - 291、成都科鸿智信科技有限公司 + - 292、北京木屋时代科技有限公司 + - 293、大学通(哈尔滨)科技有限责任公司 + - 294、浙江华坤道威数据科技有限公司 + - 295、吉祥航空【吉祥航空】 + - 296、南京圆周网络科技有限公司 + - 297、广州市洋葱omall电子商务 + - 298、天津联物科技有限公司 + - 299、跑哪儿科技(北京)有限公司 + - 300、深圳市美西西餐饮有限公司(喜茶) + - 301、平安不动产有限公司【平安】 + - 302、江苏中海昇物联科技有限公司 + - 303、湖南牙医帮科技有限公司 + - 304、重庆民航凯亚信息技术有限公司(易通航) + - 305、递易(上海)智能科技有限公司 + - 306、亚朵 + - 307、浙江新课堂教育股份有限公司 + - 308、北京蜂创科技有限公司 + - 309、德一智慧城市信息系统有限公司 + - 310、北京翼点科技有限公司 + - 311、湖南智数新维度信息科技有限公司 + - 312、北京玖扬博文文化发展有限公司 + - 313、上海宇珩信息科技有限公司 + - 314、全景智联(武汉)科技有限公司 + - 315、天津易客满国际物流有限公司 + - 316、南京爱福路汽车科技有限公司 + - 317、我房旅居集团 + - 318、湛江亲邻科技有限公司 + - 319、深圳市姜科网络有限公司 + - 320、青岛日日顺物流有限公司 + - 321、南京太川信息技术有限公司 + - 322、美图之家科技优先公司【美图】 + - 323、南京太川信息技术有限公司 + - 324、众薪科技(北京)有限公司 + - 325、武汉安安物联科技有限公司 + - 326、北京智客朗道网络科技有限公司 + - 327、深圳市超级猩猩健身管理管理有限公司 + - 328、重庆达志科技有限公司 + - 329、上海享评信息科技有限公司 + - 330、薪得付信息科技 + - 331、跟谁学 + - 332、中道(苏州)旅游网络科技有限公司 + - 333、广州小卫科技有限公司 + - 334、上海非码网络科技有限公司 + - 335、途家网网络技术(北京)有限公司【途家】 + - 336、广州辉凡信息科技有限公司 + - 337、天维尔信息科技股份有限公司 + - 338、上海极豆科技有限公司 + - 339、苏州触达信息技术有限公司 + - 340、北京热云科技有限公司 + - 341、中智企服(北京)科技有限公司 + - 342、易联云计算(杭州)有限责任公司 + - 343、青岛航空股份有限公司【青岛航空】 + - 344、山西博睿通科技有限公司 + - 345、网易杭州网络有限公司【网易】 + - 346、北京果果乐学科技有限公司 + - 347、百望股份有限公司 + - 348、中保金服(深圳)科技有限公司 + - 349、天津运友物流科技股份有限公司 + - 350、广东创能科技股份有限公司 + - 351、上海倚博信息科技有限公司 + - 352、深圳百果园实业(集团)股份有限公司 + - 353、广州细刻网络科技有限公司 + - 354、武汉鸿业众创科技有限公司 + - 355、金锡科技(广州)有限公司 + - 356、易瑞国际电子商务有限公司 + - 357、奇点云 + - 358、中视信息科技有限公司 + - 359、开源项目:datax-web + - 360、云知声智能科技股份有限公司 + - 361、开源项目:bboss + - 362、成都深驾科技有限公司 + - 363、FunPlus【趣加】 + - 364、杭州创匠信科技有限公司 + - 365、龙匠(北京)科技发展有限公司 + - 366、广州一链通互联网科技有限公司 + - 367、上海星艾网络科技有限公司 + - 368、虎博网络技术(上海)有限公司 + - 369、青岛优米信息技术有限公司 + - 370、八维通科技有限公司 + - 371、烟台合享智星数据科技有限公司 + - 372、东吴证券股份有限公司 + - 373、中通云仓股份有限公司【中通】 + - 374、北京加菲猫科技有限公司 + - 375、北京匠心演绎科技有限公司 + - 376、宝贝走天下 + - 377、厦门众库科技有限公司 + - 378、海通证券数据中心 + - 389、湖南快乐通宝小额贷款有限公司 + - 380、浙江大华技术股份有限公司 + - 381、杭州魔筷科技有限公司 + - 382、青岛掌讯通区块链科技有限公司 + - 383、新大陆金融科技 + - 384、常州玺拓软件科技有限公司 + - 385、北京正保网格教育科技有限公司 + - 386、统一企业(中国)投资有限公司【统一】 + - 387、微革网络科技有限公司 + - 388、杭州融易算科技有限公司 + - 399、青岛上啥班网络科技有限公司 + - 390、京东酒世界 + - 391、杭州爱博仕科技有限公司 + - 392、五星金服控股有限公司 + - 393、福建乐摩物联科技有限公司 + - 394、百炼智能科技有限公司 + - 395、山东能源数智云科技有限公司 + - 396、招商局能源运输股份有限公司 + - 397、三一集团【三一】 + - 398、东巴文(深圳)健康管理有限公司 + - 399、索易软件 + - 400、深圳市宁远科技有限公司 + - 401、熙牛医疗 + - 402、南京智鹤电子科技有限公司 + - 403、嘀嗒出行【嘀嗒出行】 + - 404、广州虎牙信息科技有限公司【虎牙】 + - 405、广州欧莱雅百库网络科技有限公司【欧莱雅】 + - 406、微微科技有限公司 + - 407、我爱我家房地产经纪有限公司【我爱我家】 + - 408、九号发现 + - 409、薪人薪事 + - 410、武汉氪细胞网络技术有限公司 + - 411、广州市斯凯奇商业有限公司 + - 412、微淼商学院 + - 413、杭州车盛科技有限公司 + - 414、深兰科技(上海)有限公司 + - 415、安徽中科美络信息技术有限公司 + - 416、比亚迪汽车工业有限公司【比亚迪】 + - 417、湖南小桔信息技术有限公司 + - 418、安徽科大国创软件科技有限公司 + - 419、克而瑞 + - 420、陕西云基华海信息技术有限公司 + - 421、安徽深宁科技有限公司 + - 422、广东康爱多数字健康有限公司 + - 423、嘉里电子商务 + - 424、上海时代光华教育发展有限公司 + - 425、CityDo + - 426、上海禹知信息科技有限公司 + - 427、广东智瑞科技有限公司 + - 428、西安爱铭网络科技有限公司 + - 429、心医国际数字医疗系统(大连)有限公司 + - 430、乐其电商 + - 431、锐达科技 + - 432、天津长城滨银汽车金融有限公司 + - 433、代码网 + - 434、东莞市东城乔伦软件开发工作室 + - 435、浙江百应科技有限公司 + - 436、上海力爱帝信息技术有限公司(Red E) + - 437、云徙科技有限公司 + - 438、北京康智乐思网络科技有限公司【大姨吗APP】 + - 439、安徽开元瞬视科技有限公司 + - 440、立方 + - 441、厦门纵行科技 + - 442、乐山-菲尼克斯半导体有限公司 + - 443、武汉光谷联合集团有限公司 + - 444、上海金仕达软件科技有限公司 + - 445、深圳易世通达科技有限公司 + - 446、爱动超越人工智能科技(北京)有限责任公司 + - 447、迪普信(北京)科技有限公司 + - 448、掌站科技(北京)有限公司 + - 449、深圳市华云中盛股份有限公司 + - 450、上海原圈科技有限公司 + - 451、广州赞赏信息科技有限公司 + - 452、Amber Group + - 453、德威国际货运代理(上海)公司 + - 454、浙江杰夫兄弟智慧科技有限公司 + - 455、信也科技 + - 456、开思时代科技(深圳)有限公司 + - 457、大连槐德科技有限公司 + - 458、同程生活 + - 459、松果出行 + - 460、企鹅杏仁集团 + - 461、宁波科云信息科技有限公司 + - 462、上海格蓝威驰信息科技有限公司 + - 463、杭州趣淘鲸科技有限公司 + - 464、湖州市数字惠民科技有限公司 + - 465、乐普(北京)医疗器械股份有限公司 + - 466、广州市晴川高新技术开发有限公司 + - 467、山西缇客科技有限公司 + - 468、徐州卡西穆电子商务有限公司 + - 469、格创东智科技有限公司 + - 470、世纪龙信息网络有限责任公司 + - 471、邦道科技有限公司 + - 472、河南中盟新云科技股份有限公司 + - 473、横琴人寿保险有限公司 + - 474、上海海隆华钟信息技术有限公司 + - 475、上海久湛 + - 476、上海仙豆智能机器人有限公司 + - 477、广州汇尚网络科技有限公司 + - 478、深圳市阿卡索资讯股份有限公司 + - 479、青岛佳家康健康管理有限责任公司 + - 480、蓝城兄弟 + - 481、成都天府通金融服务股份有限公司 + - 482、深圳云镖网络科技有限公司 + - 483、上海影创科技 + - 484、成都艾拉物联 + - 485、北京客邻尚品网络技术有限公司 + - 486、IT实战联盟 + - 487、杭州尤拉夫科技有限公司 + - 488、中大检测(湖南)股份有限公司 + - 489、江苏电老虎工业互联网股份有限公司 + - 490、上海助通信息科技有限公司 + - 491、北京符节科技有限公司 + - 492、杭州英祐科技有限公司 + - 493、江苏电老虎工业互联网股份有限公司 + - 494、深圳市点猫科技有限公司 + - 495、杭州天音 + - 496、深圳市二十一科技互联网有限公司 + - 497、海南海口翎度科技 + - 498、北京小趣智品科技有限公司 + - 499、广州石竹计算机软件有限公司 + - 500、深圳市惟客数据科技有限公司 + - 501、中国医疗器械有限公司 + - 502、上海云谦科技有限公司 + - 503、上海磐农信息科技有限公司 + - 504、广州领航食品有限公司 + - 505、青岛掌讯通区块链科技有限公司 + - 506、北京新网数码信息技术有限公司 + - 507、超体信息科技(深圳)有限公司 + - 508、长沙店帮手信息科技有限公司 + - 509、上海助弓装饰工程有限公司 + - 510、杭州寻联网络科技有限公司 + - 511、成都大淘客科技有限公司 + - 512、松果出行 + - 513、深圳市唤梦科技有限公司 + - 514、上汽集团商用车技术中心 + - 515、北京中航讯科技股份有限公司 + - 516、北龙中网(北京)科技有限责任公司 + - 517、前海超级前台(深圳)信息技术有限公司 + - 518、上海中商网络股份有限公司 + - 519、上海助通信息科技有限公司 + - 520、宁波聚臻智能科技有限公司 + - 521、上海零动数码科技股份有限公司 + - 522、浙江学海教育科技有限公司 + - 523、聚学云(山东)信息技术有限公司 + - 524、多氟多新材料股份有限公司 + - 525、智慧眼科技股份有限公司 + - 526、广东智通人才连锁股份有限公司 + - 527、世纪开元智印互联科技集团股份有限公司 + - 528、北京理想汽车【理想汽车】 + - 529、巽逸科技(重庆)有限公司 + - 530、义乌购电子商务有限公司 + - 531、深圳市珂莱蒂尔服饰有限公司 + - 532、江西国泰利民信息科技有限公司 + - 533、广西广电大数据科技有限公司 + - 534、杭州艾麦科技有限公司 + - 535、广州小滴科技有限公司 + - 536、佳缘科技股份有限公司 + - 537、上海深擎信息科技有限公司 + - 538、武商网 + - 539、福建民本信息科技有限公司 + - 540、杭州惠合信息科技有限公司 + - 541、厦门爱立得科技有限公司 + - 542、成都拟合未来科技有限公司 + - 543、宁波聚臻智能科技有限公司 + - 544、广东百慧科技有限公司 + - 545、笨马网络 + - 546、深圳市信安数字科技有限公司 + - 547、深圳市思乐数据技术有限公司 + - 548、四川绿源集科技有限公司 + - 549、湖南云医链生物科技有限公司 + - 550、杭州源诚科技有限公司 + - 551、北京开课吧科技有限公司 + - 552、北京多来点信息技术有限公司 + - 553、JEECG BOOT低代码开发平台 + - 554、苏州同元软控信息技术有限公司 + - 555、江苏大泰信息技术有限公司 + - 556、北京大禹汇智 + - 557、北京盛哲科技有限公司 + - …… + +> 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 + +欢迎大家的关注和使用,XXL-JOB也将拥抱变化,持续发展。 + + +### 1.5 下载 + +#### 文档地址 + +- [中文文档](https://www.xuxueli.com/xxl-job/) +- [English Documentation](https://www.xuxueli.com/xxl-job/en/) + +#### 源码仓库地址 + +源码仓库地址 | Release Download +--- | --- +[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job) | [Download](https://github.com/xuxueli/xxl-job/releases) +[http://gitee.com/xuxueli0323/xxl-job](http://gitee.com/xuxueli0323/xxl-job) | [Download](http://gitee.com/xuxueli0323/xxl-job/releases) + + +#### 中央仓库地址 + +``` +<!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ --> +<dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>${最新稳定版本}</version> +</dependency> +``` + + +### 1.6 环境 +- Maven3+ +- Jdk1.8+ +- Mysql5.7+ + + +## 二、快速入门 + +### 2.1 初始化“调度数据库” +请下载项目源码并解压,获取 "调度数据库初始化SQL脚本" 并执行即可。 + +"调度数据库初始化SQL脚本" 位置为: + + /xxl-job/doc/db/tables_xxl_job.sql + +调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例; + +如果mysql做主从,调度中心集群节点务必强制走主库; + +### 2.2 编译源码 +解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可,源码结构如下: + + xxl-job-admin:调度中心 + xxl-job-core:公共依赖 + xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器) + :xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式; + :xxl-job-executor-sample-frameless:无框架版本; + + +### 2.3 配置部署“调度中心” + + 调度中心项目:xxl-job-admin + 作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。 + +#### 步骤一:调度中心配置: +调度中心配置文件地址: + + /xxl-job/xxl-job-admin/src/main/resources/application.properties + + +调度中心配置内容说明: + + ### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致 + spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai + spring.datasource.username=root + spring.datasource.password=root_pwd + spring.datasource.driver-class-name=com.mysql.jdbc.Driver + + ### 报警邮箱 + spring.mail.host=smtp.qq.com + spring.mail.port=25 + spring.mail.username=xxx@qq.com + spring.mail.password=xxx + spring.mail.properties.mail.smtp.auth=true + spring.mail.properties.mail.smtp.starttls.enable=true + spring.mail.properties.mail.smtp.starttls.required=true + spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory + + ### 调度中心通讯TOKEN [选填]:非空时启用; + xxl.job.accessToken= + + ### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文; + xxl.job.i18n=zh_CN + + ## 调度线程池最大线程配置【必填】 + xxl.job.triggerpool.fast.max=200 + xxl.job.triggerpool.slow.max=100 + + ### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能; + xxl.job.logretentiondays=30 + + + +#### 步骤二:部署项目: +如果已经正确进行上述配置,可将项目编译打包部署。 + +调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址) + +默认登录账号 "admin/123456", 登录后运行界面如下图所示。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_6yC0.png "在这里输入图片标题") + +至此“调度中心”项目已经部署成功。 + +#### 步骤三:调度中心集群(可选): +调度中心支持集群部署,提升调度系统容灾和可用性。 + +调度中心集群部署时,几点要求和建议: +- DB配置保持一致; +- 集群机器时钟保持一致(单机集群忽视); +- 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。 + + +#### 其他:Docker 镜像方式搭建调度中心: + +- 下载镜像 + +``` +// Docker地址:https://hub.docker.com/r/xuxueli/xxl-job-admin/ (建议指定版本号) +docker pull xuxueli/xxl-job-admin +``` + +- 创建容器并运行 + +``` +docker run -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:{指定版本} + +/** +* 如需自定义 mysql 等配置,可通过 "-e PARAMS" 指定,参数格式 PARAMS="--key=value --key2=value2" ; +* 配置项参考文件:/xxl-job/xxl-job-admin/src/main/resources/application.properties +* 如需自定义 JVM内存参数 等配置,可通过 "-e JAVA_OPTS" 指定,参数格式 JAVA_OPTS="-Xmx512m" ; +*/ +docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:{指定版本} +``` + + +### 2.4 配置部署“执行器项目” + + “执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例,可直接使用,也可以参考其并将现有项目改造成执行器) + 作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。 + +#### 步骤一:maven依赖 +确认pom文件中引入了 "xxl-job-core" 的maven依赖; + +#### 步骤二:执行器配置 +执行器配置,配置文件地址: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties + +执行器配置,配置内容说明: + + ### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; + xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + + ### 执行器通讯TOKEN [选填]:非空时启用; + xxl.job.accessToken= + + ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 + xxl.job.executor.appname=xxl-job-executor-sample + ### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。 + xxl.job.executor.address= + ### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; + xxl.job.executor.ip= + ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; + xxl.job.executor.port=9999 + ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; + xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler + ### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能; + xxl.job.executor.logretentiondays=30 + + +#### 步骤三:执行器组件配置 + +执行器组件,配置文件地址: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java + +执行器组件,配置内容说明: + +``` +@Bean +public XxlJobSpringExecutor xxlJobExecutor() { + logger.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(adminAddresses); + xxlJobSpringExecutor.setAppname(appname); + xxlJobSpringExecutor.setIp(ip); + xxlJobSpringExecutor.setPort(port); + xxlJobSpringExecutor.setAccessToken(accessToken); + xxlJobSpringExecutor.setLogPath(logPath); + xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); + + return xxlJobSpringExecutor; +} +``` + +#### 步骤四:部署执行器项目: +如果已经正确进行上述配置,可将执行器项目编译打部署,系统提供多种执行器Sample示例项目,选择其中一个即可,各自的部署方式如下。 + + xxl-job-executor-sample-springboot:项目编译打包成springboot类型的可执行JAR包,命令启动即可; + xxl-job-executor-sample-frameless:项目编译打包成JAR包,命令启动即可; + + +至此“执行器”项目已经部署结束。 + +#### 步骤五:执行器集群(可选): +执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。 + +执行器集群部署时,几点要求和建议: +- 执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。 +- 同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。 + + +### 2.5 开发第一个任务“Hello World” +本示例以新建一个 “GLUE模式(Java)” 运行模式的任务为例。更多有关任务的详细配置,请查看“章节三:任务详解”。 +( “GLUE模式(Java)”的执行代码托管到调度中心在线维护,相比“Bean模式任务”需要在执行器项目开发部署上线,更加简便轻量) + +> 前提:请确认“调度中心”和“执行器”项目已经成功部署并启动; + +#### 步骤一:新建任务: +登录调度中心,点击下图所示“新建任务”按钮,新建示例任务。然后,参考下面截图中任务的参数配置,点击保存。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_o8HQ.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题") + + +#### 步骤二:“GLUE模式(Java)” 任务开发: +请点击任务右侧 “GLUE” 按钮,进入 “GLUE编辑器开发界面” ,见下图。“GLUE模式(Java)” 运行模式的任务默认已经初始化了示例任务代码,即打印Hello World。 +( “GLUE模式(Java)” 运行模式的任务实际上是一段继承自IJobHandler的Java类代码,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务,详细介绍请查看第三章节) + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Fgql.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +#### 步骤三:触发执行: +请点击任务右侧 “执行” 按钮,可手动触发一次任务执行(通常情况下,通过配置Cron表达式进行任务调度触发)。 + +#### 步骤四:查看日志: +请点击任务右侧 “日志” 按钮,可前往任务日志界面查看任务日志。 +在任务日志界面中,可查看该任务的历史调度记录以及每一次调度的任务调度信息、执行参数和执行信息。运行中的任务点击右侧的“执行日志”按钮,可进入日志控制台查看实时执行日志。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题") + +在日志控制台,可以Rolling方式实时查看任务在执行器一侧运行输出的日志信息,实时监控任务进度; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_eYrv.png "在这里输入图片标题") + +## 三、任务详解 + +### 配置属性详细说明: + + 基础配置: + - 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置; + - 任务描述:任务的描述信息,便于任务管理; + - 负责人:任务的负责人; + - 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔; + + 触发配置: + - 调度类型: + 无:该类型不会主动触发调度; + CRON:该类型将会通过CRON,触发任务调度; + 固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发; + 固定延迟:该类型将会以固定延迟,触发任务调度;按照固定的延迟时间,从上次调度结束后开始计算延迟时间,到达延迟时间后触发下次调度; + - CRON:触发任务执行的Cron表达式; + - 固定速度:固件速度的时间间隔,单位为秒; + - 固定延迟:固件延迟的时间间隔,单位为秒; + + 任务配置: + - 运行模式: + BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务; + GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务; + GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本; + GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本; + GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本; + GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本; + GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本; + - JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值; + - 执行参数:任务执行所需的参数; + + 高级配置: + - 路由策略:当执行器集群部署时,提供丰富的路由策略,包括; + FIRST(第一个):固定选择第一个机器; + LAST(最后一个):固定选择最后一个机器; + ROUND(轮询):; + RANDOM(随机):随机选择在线的机器; + CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。 + LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举; + LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举; + FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度; + BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; + SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; + - 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。 + - 调度过期策略: + - 忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间; + - 立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间; + - 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略; + 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行; + 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败; + 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务; + - 任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务; + - 失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; + + + + + +### 3.1 BEAN模式(类形式) + +Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。 + +- 优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持,可以参考示例项目 "xxl-job-executor-sample-frameless"; +- 缺点: + - 每个任务需要占用一个Java类,造成类的浪费; + - 不支持自动扫描任务并注入到执行器容器,需要手动注入。 + +#### 步骤一:执行器项目中,开发Job类: + + 1、开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。 + 2、手动通过如下方式注入到执行器容器。 + ``` + XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler()); + ``` + +#### 步骤二:调度中心,新建调度任务 +后续步骤和 "3.2 BEAN模式(方法形式)"一致,可以前往参考。 + + +### 3.2 BEAN模式(方法形式) + +Bean模式任务,支持基于方法的开发方式,每个任务对应一个方法。 + +- 优点: + - 每个任务只需要开发一个方法,并添加"@XxlJob"注解即可,更加方便、快速。 + - 支持自动扫描任务并注入到执行器容器。 +- 缺点:要求Spring容器环境; + +>基于方法开发的任务,底层会生成JobHandler代理,和基于类的方式一样,任务也会以JobHandler的形式存在于执行器任务容器中。 + +#### 步骤一:执行器项目中,开发Job方法: + + 1、任务开发:在Spring Bean实例中,开发Job方法; + 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 + 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志; + 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果; + +``` +// 可参考Sample示例执行器中的 "com.xxl.job.executor.service.jobhandler.SampleXxlJob" ,如下: +@XxlJob("demoJobHandler") +public void demoJobHandler() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); +} +``` + +#### 步骤二:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "BEAN模式",JobHandler属性填写任务注解“@XxlJob”中定义的值; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题") + +#### 原生内置Bean模式任务 +为方便用户参考与快速实用,示例执行器内原生提供多个Bean模式任务Handler,可以直接配置实用,如下: + +- demoJobHandler:简单示例任务,任务内部模拟耗时任务逻辑,用户可在线体验Rolling Log等功能; +- shardingJobHandler:分片示例任务,任务内部模拟处理分片参数,可参考熟悉分片任务; +- httpJobHandler:通用HTTP任务Handler;业务方只需要提供HTTP链接等信息即可,不限制语言、平台。示例任务入参如下: + ``` + url: http://www.xxx.com + method: get 或 post + data: post-data + ``` +- commandJobHandler:通用命令行任务Handler;业务方只需要提供命令行即可;如 “pwd”命令; + + +### 3.3 GLUE模式(Java) +任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。开发流程如下: + +#### 步骤一:调度中心,新建调度任务: +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Java)"; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tJOq.png "在这里输入图片标题") + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +版本回溯功能(支持30个版本的版本回溯):在GLUE任务的Web IDE界面,选择右上角下拉框“版本回溯”,会列出该GLUE的更新历史,选择相应版本即可显示该版本代码,保存后GLUE代码即回退到对应的历史版本; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +### 3.4 GLUE模式(Shell) + +#### 步骤一:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Shell)"; + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +该模式的任务实际上是一段 "shell" 脚本; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_iUw0.png "在这里输入图片标题") + +### 3.4 GLUE模式(Python) + +#### 步骤一:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Python)"; + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +该模式的任务实际上是一段 "python" 脚本; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_BPLG.png "在这里输入图片标题") + +### 3.5 GLUE模式(NodeJS) + +#### 步骤一:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(NodeJS)"; + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +该模式的任务实际上是一段 "nodeJS" 脚本; + +### 3.6 GLUE模式(PHP) +同上 + +### 3.7 GLUE模式(PowerShell) +同上 + + + +## 四、操作指南 + +### 4.1 配置执行器 +点击进入"执行器管理"界面, 如下图: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Hr2T.png "在这里输入图片标题") + + 1、"调度中心OnLine:"右侧显示在线的"调度中心"列表, 任务执行结束后, 将会以failover的模式进行回调调度中心通知执行结果, 避免回调的单点风险; + 2、"执行器列表" 中显示在线的执行器列表, 可通过"OnLine 机器"查看对应执行器的集群机器。 + +点击按钮 "+新增执行器" 弹框如下图, 可新增执行器配置: + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_V3vF.png "在这里输入图片标题") + +执行器属性说明 + + AppName: 是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用; + 名称: 执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性; + 排序: 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表; + 注册方式:调度中心获取执行器地址的方式; + 自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址; + 手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用; + 机器地址:"注册方式"为"手动录入"时有效,支持人工维护执行器的地址信息; + +### 4.2 新建任务 +进入任务管理界面,点击“新增任务”按钮,在弹出的“新增任务”界面配置任务属性后保存即可。详情页参考章节 "三、任务详解"。 + +### 4.3 编辑任务 +进入任务管理界面,选中指定任务。点击该任务右侧“编辑”按钮,在弹出的“编辑任务”界面更新任务属性后保存即可,可以修改设置的任务属性信息: + +### 4.4 编辑GLUE代码 + +该操作仅针对GLUE任务。 + +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发。可参考章节 "3.3 GLUE模式(Java)"。 + +### 4.5 启动/停止任务 +可对任务进行“启动”和“停止”操作。 +需要注意的是,此处的启动/停止仅针对任务的后续调度触发行为,不会影响到已经触发的调度任务,如需终止已经触发的调度任务,可查看“4.9 终止运行中的任务” + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +### 4.6 手动触发一次调度 +点击“执行”按钮,可手动触发一次任务调度,不影响原有调度规则。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +### 4.7 查看调度日志 +点击“日志”按钮,可以查看任务历史调度日志。在历史调入日志界面可查看每次任务调度的调度结果、执行结果等,点击“执行日志”按钮可查看执行器完整日志。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_UDSo.png "在这里输入图片标题") + + 调度时间:"调度中心"触发本次调度并向"执行器"发送任务执行信号的时间; + 调度结果:"调度中心"触发本次调度的结果,200表示成功,500或其他表示失败; + 调度备注:"调度中心"触发本次调度的日志信息; + 执行器地址:本次任务执行的机器地址 + 运行模式:触发调度时任务的运行模式,运行模式可参考章节 "三、任务详解"; + 任务参数:本地任务执行的入参 + 执行时间:"执行器"中本次任务执行结束后回调的时间; + 执行结果:"执行器"中本次任务执行的结果,200表示成功,500或其他表示失败; + 执行备注:"执行器"中本次任务执行的日志信息; + 操作: + "执行日志"按钮:点击可查看本地任务执行的详细日志信息;详见“4.8 查看执行日志”; + "终止任务"按钮:点击可终止本地调度对应执行器上本任务的执行线程,包括未执行的阻塞任务一并被终止; + +### 4.8 查看执行日志 +点击执行日志右侧的 “执行日志” 按钮,可跳转至执行日志界面,可以查看业务代码中打印的完整日志,如下图; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tvGI.png "在这里输入图片标题") + +### 4.9 终止运行中的任务 +仅针对执行中的任务。 +在任务日志界面,点击右侧的“终止任务”按钮,将会向本次任务对应的执行器发送任务终止请求,将会终止掉本次任务,同时会清空掉整个任务执行队列。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_hIci.png "在这里输入图片标题") + +任务终止时通过 "interrupt" 执行线程的方式实现, 将会触发 "InterruptedException" 异常。因此如果JobHandler内部catch到了该异常并消化掉的话, 任务终止功能将不可用。 + +因此, 如果遇到上述任务终止不可用的情况, 需要在JobHandler中应该针对 "InterruptedException" 异常进行特殊处理 (向上抛出) , 正确逻辑如下: +``` +try{ + // do something +} catch (Exception e) { + if (e instanceof InterruptedException) { + throw e; + } + logger.warn("{}", e); +} +``` + +而且,在JobHandler中开启子线程时,子线程也不可catch处理"InterruptedException",应该主动向上抛出。 + +任务终止时会执行对应JobHandler的"destroy()"方法,可以借助该方法处理一些资源回收的逻辑。 + + +### 4.10 删除执行日志 +在任务日志界面,选中执行器和任务之后,点击右侧的"删除"按钮将会出现"日志清理"弹框,弹框中支持选择不同类型的日志清理策略,选中后点击"确定"按钮即可进行日志清理操作; +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Ypik.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_EB65.png "在这里输入图片标题") + +### 4.11 删除任务 +点击删除按钮,可以删除对应任务。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z9Qr.png "在这里输入图片标题") + +### 4.12 用户管理 +进入 "用户管理" 界面,可查看和管理用户信息; + +目前用户分为两种角色: +- 管理员:拥有全量权限,支持在线管理用户信息,为用户分配权限,权限分配粒度为执行器; +- 普通用户:仅拥有被分配权限的执行器,及相关任务的操作权限; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_1001.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_1002.png "在这里输入图片标题") + + +## 五、总体设计 +### 5.1 源码目录介绍 + - /doc :文档资料 + - /db :“调度数据库”建表脚本 + - /xxl-job-admin :调度中心,项目源码 + - /xxl-job-core :公共Jar依赖 + - /xxl-job-executor-samples :执行器,Sample示例项目(大家可以在该项目上进行开发,也可以将现有项目改造生成执行器项目) + +### 5.2 “调度数据库”配置 +XXL-JOB调度模块基于自研调度组件并支持集群部署,调度数据库表说明如下: + + - xxl_job_lock:任务调度锁表; + - xxl_job_group:执行器信息表,维护任务执行器信息; + - xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等; + - xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等; + - xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到; + - xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能; + - xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息; + - xxl_job_user:系统用户表; + + +### 5.3 架构设计 +#### 5.3.1 设计思想 +将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。 + +将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。 + +因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性; + +#### 5.3.2 系统组成 +- **调度模块(调度中心)**: + 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; + 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。 +- **执行模块(执行器)**: + 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; + 接收“调度中心”的执行请求、终止请求和日志请求等。 + +#### 5.3.3 架构图 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png "在这里输入图片标题") + +### 5.4 调度模块剖析 +#### 5.4.1 quartz的不足 +Quartz作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中Quartz采用API的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题: + +- 问题一:调用API的的方式操作任务,不人性化; +- 问题二:需要持久化业务QuartzJobBean到底层数据表中,系统侵入性相当严重。 +- 问题三:调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务; +- 问题四:quartz底层以“抢占式”获取DB锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而XXL-JOB通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。 + +XXL-JOB弥补了quartz的上述不足之处。 + +#### 5.4.2 自研调度模块 +XXL-JOB最终选择自研调度组件(早期调度组件基于Quartz);一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性; + +XXL-JOB中“调度模块”和“任务模块”完全解耦,调度模块进行任务调度时,将会解析不同的任务参数发起远程调用,调用各自的远程执行器服务。这种调用模型类似RPC调用,调度中心提供调用代理的功能,而执行器提供远程服务的功能。 + +#### 5.4.3 调度中心HA(集群) +基于数据库的集群方案,数据库选用Mysql;集群分布式并发环境中进行定时任务调度时,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。 + +#### 5.4.4 调度线程池 +调度采用线程池方式实现,避免单线程因阻塞而引起任务调度延迟。 + +#### 5.4.5 并行调度 +XXL-JOB调度模块默认采用并行机制,在多线程调度的情况下,调度模块被阻塞的几率很低,大大提高了调度系统的承载量。 + +XXL-JOB的不同任务之间并行调度、并行执行。 +XXL-JOB的单个任务,针对多个执行器是并行运行的,针对单个执行器是串行执行的。同时支持任务终止。 + +#### 5.4.6 过期处理策略 +任务调度错过触发时间时的处理策略: +- 可能原因:服务重启;调度线程被阻塞,线程被耗尽;上次调度持续阻塞,下次调度被错过; +- 处理策略: + - 过期超5s:本次忽略,当前时间开始计算下次触发时间 + - 过期5s内:立即触发一次,当前时间开始计算下次触发时间 + + +#### 5.4.7 日志回调服务 +调度模块的“调度中心”作为Web服务部署时,一方面承担调度中心功能,另一方面也为执行器提供API服务。 + +调度中心提供的"日志回调服务API服务"代码位置如下: +``` +xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback +``` + +“执行器”在接收到任务执行请求后,执行任务,在执行结束之后会将执行结果回调通知“调度中心”: + +#### 5.4.8 任务HA(Failover) +执行器如若集群部署,调度中心将会感知到在线的所有执行器,如“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”。 + +当任务"路由策略"选择"故障转移(FAILOVER)"时,当调度中心每次发起调度请求时,会按照顺序对执行器发出心跳检测请求,第一个检测为存活状态的执行器将会被选定并发送调度请求。 + +调度成功后,可在日志监控界面查看“调度备注”,如下; +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jrdI.png "在这里输入图片标题") + +“调度备注”可以看出本地调度运行轨迹,执行器的"注册方式"、"地址列表"和任务的"路由策略"。"故障转移(FAILOVER)"路由策略下,调度中心首先对第一个地址进行心跳检测,心跳失败因此自动跳过,第二个依然心跳检测失败…… +直至心跳检测第三个地址“127.0.0.1:9999”成功,选定为“目标执行器”;然后对“目标执行器”发送调度请求,调度流程结束,等待执行器回调执行结果。 + +#### 5.4.9 调度日志 +调度中心每次进行任务调度,都会记录一条任务日志,任务日志主要包括以下三部分内容: + +- 任务信息:包括“执行器地址”、“JobHandler”和“执行参数”等属性,点击任务ID按钮可查看,根据这些参数,可以精确的定位任务执行的具体机器和任务代码; +- 调度信息:包括“调度时间”、“调度结果”和“调度日志”等,根据这些参数,可以了解“调度中心”发起调度请求时具体情况。 +- 执行信息:包括“执行时间”、“执行结果”和“执行日志”等,根据这些参数,可以了解在“执行器”端任务执行的具体情况; + +调度日志,针对单次调度,属性说明如下: +- 执行器地址:任务执行的机器地址; +- JobHandler:Bean模式表示任务执行的JobHandler名称; +- 任务参数:任务执行的入参; +- 调度时间:调度中心,发起调度的时间; +- 调度结果:调度中心,发起调度的结果,SUCCESS或FAIL; +- 调度备注:调度中心,发起调度的备注信息,如地址心跳检测日志等; +- 执行时间:执行器,任务执行结束后回调的时间; +- 执行结果:执行器,任务执行的结果,SUCCESS或FAIL; +- 执行备注:执行器,任务执行的备注信息,如异常日志等; +- 执行日志:任务执行过程中,业务代码中打印的完整执行日志,见“4.8 查看执行日志”; + +#### 5.4.10 任务依赖 +原理:XXL-JOB中每个任务都对应有一个任务ID,同时,每个任务支持设置属性“子任务ID”,因此,通过“任务ID”可以匹配任务依赖关系。 + +当父任务执行结束并且执行成功时,将会根据“子任务ID”匹配子任务依赖,如果匹配到子任务,将会主动触发一次子任务的执行。 + +在任务日志界面,点击任务的“执行备注”的“查看”按钮,可以看到匹配子任务以及触发子任务执行的日志信息,如无信息则表示未触发子任务执行,可参考下图。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Wb2o.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jOAU.png "在这里输入图片标题") + +#### 5.4.11 全异步化 & 轻量级 + +- 全异步化设计:XXL-JOB系统中业务逻辑在远程执行器执行,触发流程全异步化设计。相比直接在调度中心内部执行业务逻辑,极大的降低了调度线程占用时间; + - 异步调度:调度中心每次任务触发时仅发送一次调度请求,该调度请求首先推送“异步调度队列”,然后异步推送给远程执行器 + - 异步执行:执行器会将请求存入“异步执行队列”并且立即响应调度中心,异步运行。 +- 轻量级设计:XXL-JOB调度中心中每个JOB逻辑非常 “轻”,在全异步化的基础上,单个JOB一次运行平均耗时基本在 "10ms" 之内(基本为一次请求的网络开销);因此,可以保证使用有限的线程支撑大量的JOB并发运行; + +得益于上述两点优化,理论上默认配置下的调度中心,单机能够支撑 5000 任务并发运行稳定运行; + +实际场景中,由于调度中心与执行器网络ping延迟不同、DB读写耗时不同、任务调度密集程度不同,会导致任务量上限会上下波动。 + +如若需要支撑更多的任务量,可以通过 "调大调度线程数" 、"降低调度中心与执行器ping延迟" 和 "提升机器配置" 几种方式优化。 + +#### 5.4.12 均衡调度 +调度中心在集群部署时会自动进行任务平均分配,触发组件每次获取与线程池数量(调度中心支持自定义调度线程池大小)相关数量的任务,避免大量任务集中在单个调度中心集群节点; + +### 5.5 任务 "运行模式" 剖析 +#### 5.5.1 "Bean模式" 任务 +开发步骤:可参考 "章节三" ; +原理:每个Bean模式任务都是一个Spring的Bean类实例,它被维护在“执行器”项目的Spring容器中。任务类需要加“@JobHandler(value="名称")”注解,因为“执行器”会根据该注解识别Spring容器中的任务。任务类需要继承统一接口“IJobHandler”,任务逻辑在execute方法中开发,因为“执行器”在接收到调度中心的调度请求时,将会调用“IJobHandler”的execute方法,执行任务逻辑。 + +#### 5.5.2 "GLUE模式(Java)" 任务 +开发步骤:可参考 "章节三" ; +原理:每个 "GLUE模式(Java)" 任务的代码,实际上是“一个继承自“IJobHandler”的实现类的类代码”,“执行器”接收到“调度中心”的调度请求时,会通过Groovy类加载器加载此代码,实例化成Java对象,同时注入此代码中声明的Spring服务(请确保Glue代码中的服务和类引用在“执行器”项目中存在),然后调用该对象的execute方法,执行任务逻辑。 + +#### 5.5.3 GLUE模式(Shell) + GLUE模式(Python) + GLUE模式(PHP) + GLUE模式(NodeJS) + GLUE模式(Powershell) +开发步骤:可参考 "章节三" ; +原理:脚本任务的源码托管在调度中心,脚本逻辑在执行器运行。当触发脚本任务时,执行器会加载脚本源码在执行器机器上生成一份脚本文件,然后通过Java代码调用该脚本;并且实时将脚本输出日志写到任务日志文件中,从而在调度中心可以实时监控脚本运行情况; + +目前支持的脚本类型如下: + + - shell脚本:任务运行模式选择为 "GLUE模式(Shell)"时支持 "Shell" 脚本任务; + - python脚本:任务运行模式选择为 "GLUE模式(Python)"时支持 "Python" 脚本任务; + - php脚本:任务运行模式选择为 "GLUE模式(PHP)"时支持 "PHP" 脚本任务; + - nodejs脚本:任务运行模式选择为 "GLUE模式(NodeJS)"时支持 "NodeJS" 脚本任务; + - powershell:任务运行模式选择为 "GLUE模式(PowerShell)"时支持 "PowerShell" 脚本任务; + +脚本任务通过 Exit Code 判断任务执行结果,状态码可参考章节 "5.15 任务执行结果说明"; + +#### 5.5.4 执行器 +执行器实际上是一个内嵌的Server,默认端口9999(配置项:xxl.job.executor.port)。 + +在项目启动时,执行器会通过“@JobHandler”识别Spring容器中“Bean模式任务”,以注解的value属性为key管理起来。 + +“执行器”接收到“调度中心”的调度请求时,如果任务类型为“Bean模式”,将会匹配Spring容器中的“Bean模式任务”,然后调用其execute方法,执行任务逻辑。如果任务类型为“GLUE模式”,将会加载GLue代码,实例化Java对象,注入依赖的Spring服务(注意:Glue代码中注入的Spring服务,必须存在与该“执行器”项目的Spring容器中),然后调用execute方法,执行任务逻辑。 + +#### 5.5.5 任务日志 +XXL-JOB会为每次调度请求生成一个单独的日志文件,需要通过 "XxlJobHelper.log" 打印执行日志,“调度中心”查看执行日志时将会加载对应的日志文件。 + +(历史版本通过重写LOG4J的Appender实现,存在依赖限制,该方式在新版本已经被抛弃) + +日志文件存放的位置可在“执行器”配置文件进行自定义,默认目录格式为:/data/applogs/xxl-job/jobhandler/“格式化日期”/“数据库调度日志记录的主键ID.log”。 + +在JobHandler中开启子线程时,子线程将会将会把日志打印在父线程即JobHandler的执行日志中,方便日志追踪。 + +### 5.6 通讯模块剖析 + +#### 5.6.1 一次完整的任务调度通讯流程 + - 1、“调度中心”向“执行器”发送http调度请求: “执行器”中接收请求的服务,实际上是一台内嵌Server,默认端口9999; + - 2、“执行器”执行任务逻辑; + - 3、“执行器”http回调“调度中心”调度结果: “调度中心”中接收回调的服务,是针对执行器开放一套API服务; + +#### 5.6.2 通讯数据加密 +调度中心向执行器发送的调度请求时使用RequestModel和ResponseModel两个对象封装调度请求参数和响应数据, 在进行通讯之前底层会将上述两个对象对象序列化,并进行数据协议以及时间戳检验,从而达到数据加密的功能; + +### 5.7 任务注册, 任务自动发现 +自v1.5版本之后, 任务取消了"任务执行机器"属性, 改为通过任务注册和自动发现的方式, 动态获取远程执行器地址并执行。 + + AppName: 每个执行器机器集群的唯一标示, 任务注册以 "执行器" 为最小粒度进行注册; 每个任务通过其绑定的执行器可感知对应的执行器机器列表; + 注册表: 见"xxl_job_registry"表, "执行器" 在进行任务注册时将会周期性维护一条注册记录,即机器地址和AppName的绑定关系; "调度中心" 从而可以动态感知每个AppName在线的机器列表; + 执行器注册: 任务注册Beat周期默认30s; 执行器以一倍Beat进行执行器注册, 调度中心以一倍Beat进行动态任务发现; 注册信息的失效时间为三倍Beat; + 执行器注册摘除:执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性; + + +为保证系统"轻量级"并且降低学习部署成本,没有采用Zookeeper作为注册中心,采用DB方式进行任务注册发现; + +### 5.8 任务执行结果 +自v1.6.2之后,任务执行结果通过 "IJobHandler" 的返回值 "ReturnT" 进行判断; +当返回值符合 "ReturnT.code == ReturnT.SUCCESS_CODE" 时表示任务执行成功,否则表示任务执行失败,而且可以通过 "ReturnT.msg" 回调错误信息给调度中心; +从而,在任务逻辑中可以方便的控制任务执行结果; + +### 5.9 分片广播 & 动态分片 +执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; + +"分片广播" 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 + +"分片广播" 和普通任务开发流程一致,不同之处在于可以获取分片参数,获取分片参数进行分片业务处理。 + +- Java语言任务获取分片参数方式:BEAN、GLUE模式(Java) +``` +// 可参考Sample示例执行器中的示例任务"ShardingJobHandler"了解试用 +int shardIndex = XxlJobHelper.getShardIndex(); +int shardTotal = XxlJobHelper.getShardTotal(); +``` +- 脚本语言任务获取分片参数方式:GLUE模式(Shell)、GLUE模式(Python)、GLUE模式(Nodejs) +``` +// 脚本任务入参固定为三个,依次为:任务传参、分片序号、分片总数。以Shell模式任务为例,获取分片参数代码如下 +echo "分片序号 index = $2" +echo "分片总数 total = $3" +``` + +分片参数属性说明: + + index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号; + total:总分片数,执行器集群的总机器数量; + +该特性适用场景如: +- 1、分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍; +- 2、广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等 + +### 5.10 访问令牌(AccessToken) +为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯; + +调度中心和执行器,可通过配置项 "xxl.job.accessToken" 进行AccessToken的设置。 + +调度中心和执行器,如果需要正常通讯,只有两种设置; + +- 设置一:调度中心和执行器,均不设置AccessToken;关闭安全性校验; +- 设置二:调度中心和执行器,设置了相同的AccessToken; + +### 5.11 故障转移 & 失败重试 +一次完整任务流程包括"调度(调度中心) + 执行(执行器)"两个阶段。 + +- "故障转移"发生在调度阶段,在执行器集群部署时,如果某一台执行器发生故障,该策略支持自动进行Failover切换到一台正常的执行器机器并且完成调度请求流程。 +- "失败重试"发生在"调度 + 执行"两个阶段,支持通过自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; + +### 5.12 执行器灰度上线 +调度中心与业务解耦,只需部署一次后常年不需要维护。但是,执行器中托管运行着业务作业,作业上线和变更需要重启执行器,尤其是Bean模式任务。 +执行器重启可能会中断运行中的任务。但是,XXL-JOB得益于自建执行器与自建注册中心,可以通过灰度上线的方式,避免因重启导致的任务中断的问题。 + +步骤如下: +- 1、执行器改为手动注册,下线一半机器列表(A组),线上运行另一半机器列表(B组); +- 2、等待A组机器任务运行结束并编译上线;执行器注册地址替换为A组; +- 3、等待B组机器任务运行结束并编译上线;执行器注册地址替换为A组+B组; +操作结束; + +### 5.13 任务执行结果说明 +系统根据以下标准判断任务执行结果,可参考之。 + +-- | Bean/Glue(Java) | Glue(Shell) 等脚本任务 +--- | --- | --- +成功 | IJobHandler.SUCCESS | 0 +失败 | IJobHandler.FAIL | -1(非0状态码) + +### 5.14 任务超时控制 +支持设置任务超时时间,任务运行超时的情况下,将会主动中断任务; + +需要注意的是,任务超时中断时与任务终止机制(可查看“4.9 终止运行中的任务”)类似,也是通过 "interrupt" 中断任务,因此业务代码需要将 "InterruptedException" 外抛,否则功能不可用。 + +### 5.15 跨语言 +XXL-JOB是一个跨语言的任务调度平台,主要体现在如下几个方面: +- 1、RESTful API:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。(可参考章节 “调度中心/执行器 RESTful API” ) +- 2、多任务模式:提供Java、Python、PHP……等十来种任务模式,可参考章节 “5.5 任务 "运行模式" ”;理论上可扩展任意语言任务模式; +- 2、提供基于HTTP的任务Handler(Bean任务,JobHandler="httpJobHandler");业务方只需要提供HTTP链接等相关信息即可,不限制语言、平台;(可参考章节 “原生内置Bean模式任务” ) + +### 5.16 任务失败告警 +默认提供邮件失败告警,可扩展短信、钉钉等方式。如果需要新增一种告警方式,只需要新增一个实现 "com.xxl.job.admin.core.alarm.JobAlarm" 接口的告警实现即可。可以参考默认提供邮箱告警实现 "EmailJobAlarm"。 + +### 5.17 调度中心Docker镜像构建 +可以通过以下命令快速构建调度中心,并启动运行; +``` +mvn clean package +docker build -t xuxueli/xxl-job-admin ./xxl-job-admin +docker run --name xxl-job-admin -p 8080:8080 -d xuxueli/xxl-job-admin +``` + +### 5.20 避免任务重复执行 +调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发; +针对上述情况,可以通过结合 "单机路由策略(如:第一台、一致性哈希)" + "阻塞策略(如:单机串行、丢弃后续调度)" 来规避,最终避免任务重复执行。 + +### 5.21 命令行任务 +原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +如任务参数 "pwd" 将会执行命令并输出数据; + +### 5.22 日志自动清理 +XXL-JOB日志主要包含如下两部分,均支持日志自动清理,说明如下: +- 调度中心日志表数据:可借助配置项 "xxl.job.logretentiondays" 设置日志表数据保存天数,过期日志自动清理;详情可查看上文配置说明; +- 执行器日志文件数据:可借助配置项 "xxl.job.executor.logretentiondays" 设置日志文件数据保存天数,过期日志自动清理;详情可查看上文配置说明; + +### 5.23 调度结果丢失处理 +执行器因网络抖动回调失败或宕机等异常情况,会导致任务调度结果丢失。由于调度中心依赖执行器回调来感知调度结果,因此会导致调度日志永远处于 "运行中" 状态。 + +针对该问题,调度中心提供内置组件进行处理,逻辑为:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; + + +## 六、调度中心/执行器 RESTful API +XXL-JOB 目标是一种跨平台、跨语言的任务调度规范和协议。 + +针对Java应用,可以直接通过官方提供的调度中心与执行器,方便快速的接入和使用调度中心,可以参考上文 “快速入门” 章节。 + +针对非Java应用,可借助 XXL-JOB 的标准 RESTful API 方便的实现多语言支持。 + +- 调度中心 RESTful API: + - 说明:调度中心提供给执行器使用的API;不局限于官方执行器使用,第三方可使用该API来实现执行器; + - API列表:执行器注册、任务结果回调等; +- 执行器 RESTful API : + - 说明:执行器提供给调度中心使用的API;官方执行器默认已实现,第三方执行器需要实现并对接提供给调度中心; + - API列表:任务触发、任务终止、任务日志查询……等; + +此处 RESTful API 主要用于非Java语言定制个性化执行器使用,实现跨语言。除此之外,如果有需要通过API操作调度中心,可以个性化扩展 “调度中心 RESTful API” 并使用。 + +### 6.1 调度中心 RESTful API + +API服务位置:com.xxl.job.core.biz.AdminBiz ( com.xxl.job.admin.controller.JobApiController ) +API服务请求参考代码:com.xxl.job.adminbiz.AdminBizTest + +#### a、任务回调 +``` +说明:执行器执行完任务后,回调任务结果时使用 + +------ + +地址格式:{调度中心根地址}/api/callback + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + [{ + "logId":1, // 本次调度日志ID + "logDateTim":0, // 本次调度日志时间 + "handleCode":200, // 200 表示任务执行正常,500表示失败 + "handleMsg": null + } + }] + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### b、执行器注册 +``` +说明:执行器注册时使用,调度中心会实时感知注册成功的执行器并发起任务调度 + +------ + +地址格式:{调度中心根地址}/api/registry + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "registryGroup":"EXECUTOR", // 固定值 + "registryKey":"xxl-job-executor-example", // 执行器AppName + "registryValue":"http://127.0.0.1:9999/" // 执行器地址,内置服务跟地址 + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### c、执行器注册摘除 +``` +说明:执行器注册摘除时使用,注册摘除后的执行器不参与任务调度与执行 + +------ + +地址格式:{调度中心根地址}/api/registryRemove + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "registryGroup":"EXECUTOR", // 固定值 + "registryKey":"xxl-job-executor-example", // 执行器AppName + "registryValue":"http://127.0.0.1:9999/" // 执行器地址,内置服务跟地址 + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +### 6.2 执行器 RESTful API + +API服务位置:com.xxl.job.core.biz.ExecutorBiz +API服务请求参考代码:com.xxl.job.executorbiz.ExecutorBizTest + +#### a、心跳检测 +``` +说明:调度中心检测执行器是否在线时使用 + +------ + +地址格式:{执行器内嵌服务根地址}/beat + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### b、忙碌检测 +``` +说明:调度中心检测指定执行器上指定任务是否忙碌(运行中)时使用 + +------ + +地址格式:{执行器内嵌服务根地址}/idleBeat + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "jobId":1 // 任务ID + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### c、触发任务 +``` +说明:触发任务执行 + +------ + +地址格式:{执行器内嵌服务根地址}/run + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "jobId":1, // 任务ID + "executorHandler":"demoJobHandler", // 任务标识 + "executorParams":"demoJobHandler", // 任务参数 + "executorBlockStrategy":"COVER_EARLY", // 任务阻塞策略,可选值参考 com.xxl.job.core.enums.ExecutorBlockStrategyEnum + "executorTimeout":0, // 任务超时时间,单位秒,大于零时生效 + "logId":1, // 本次调度日志ID + "logDateTime":1586629003729, // 本次调度日志时间 + "glueType":"BEAN", // 任务模式,可选值参考 com.xxl.job.core.glue.GlueTypeEnum + "glueSource":"xxx", // GLUE脚本代码 + "glueUpdatetime":1586629003727, // GLUE脚本更新时间,用于判定脚本是否变更以及是否需要刷新 + "broadcastIndex":0, // 分片参数:当前分片 + "broadcastTotal":0 // 分片参数:总分片 + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### f、终止任务 +``` +说明:终止任务 + +------ + +地址格式:{执行器内嵌服务根地址}/kill + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "jobId":1 // 任务ID + } + + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### d、查看执行日志 +``` +说明:终止任务,滚动方式加载 + +------ + +地址格式:{执行器内嵌服务根地址}/log + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "logDateTim":0, // 本次调度日志时间 + "logId":0, // 本次调度日志ID + "fromLineNum":0 // 日志开始行号,滚动加载日志 + } + +响应数据格式: + { + "code":200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + "content":{ + "fromLineNum":0, // 本次请求,日志开始行数 + "toLineNum":100, // 本次请求,日志结束行号 + "logContent":"xxx", // 本次请求日志内容 + "isEnd":true // 日志是否全部加载完 + } + } +``` + + + +## 七、版本更新日志 +### 7.1 版本 V1.1.x,新特性[2015-12-05] +**【于V1.1.x版本,XXL-JOB正式应用于我司,内部定制别名为 “Ferrari”,新接入应用推荐使用最新版本】** +- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; +- 2、动态:支持动态修改任务状态,动态暂停/恢复任务,即时生效; +- 3、服务HA:任务信息持久化到mysql中,Job服务天然支持集群,保证服务HA; +- 4、任务HA:某台Job服务挂掉,任务会平滑分配给其他的某一台存活服务,即使所有服务挂掉,重启时或补偿执行丢失任务; +- 5、一个任务只会在其中一台服务器上执行; +- 6、任务串行执行; +- 7、支持自定义参数; +- 8、支持远程任务执行终止; + +### 7.2 版本 V1.2.x,新特性[2016-01-17] +- 1、支持任务分组; +- 2、支持“本地任务”、“远程任务”; +- 3、底层通讯支持两种方式,Servlet方式 + JETTY方式; +- 4、支持“任务日志”; +- 5、支持“串行执行”,并行执行; + + 说明:V1.2版本将系统架构按功能拆分为: + + - 调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求; + - 执行模块(执行器):负责接收调度请求并执行任务逻辑; + - 通讯模块:负责调度模块和任务模块之间的信息通讯; + 优点: + + - 解耦:任务模块提供任务接口,调度模块维护调度信息,业务相互独立; + - 高扩展性; + - 稳定性; + +### 7.3 版本 V1.3.0,新特性[2016-05-19] +- 1、遗弃“本地任务”模式,推荐使用“远程任务”,易于系统解耦,任务对应的JobHandler统称为“执行器”; +- 2、遗弃“servlet”方式底层系统通讯,推荐使用JETTY方式,调度+回调双向通讯,重构通讯逻辑; +- 3、UI交互优化:左侧菜单展开状态优化,菜单项选中状态优化,任务列表打开表格有压缩优化; +- 4、【重要】“执行器”细分为:BEAN、GLUE两种开发模式,简介见下文: + + “执行器” 模式简介: + - BEAN模式执行器:每个执行器都是Spring的一个Bean实例,XXL-JOB通过注解@JobHandler识别和调度执行器; + -GLUE模式执行器:每个执行器对应一段代码,在线Web编辑和维护,动态编译生效,执行器负责加载GLUE代码和执行; + +### 7.4 版本 V1.3.1,新特性[2016-05-23] +- 1、更新项目目录结构: + - /xxl-job-admin -------------------- 【调度中心】:负责管理调度信息,按照调度配置发出调度请求; + - /xxl-job-core ----------------------- 公共依赖 + - /xxl-job-executor-example ------ 【执行器】:负责接收调度请求并执行任务逻辑; + - /db ---------------------------------- 建表脚本 + - /doc --------------------------------- 用户手册 +- 2、在新的目录结构上,升级了用户手册; +- 3、优化了一些交互和UI; + +### 7.5 版本 V1.3.2,新特性[2016-05-28] +- 1、调度逻辑进行事务包裹; +- 2、执行器异步回调执行日志; +- 3、【重要】在 “调度中心” 支持HA的基础上,扩展执行器的Failover支持,支持配置多执行期地址; + +### 7.6 版本 V1.4.0 新特性[2016-07-24] +- 1、任务依赖: 通过事件触发方式实现, 任务执行成功并回调时会主动触发一次子任务的调度, 多个子任务用逗号分隔; +- 2、执行器底层实现代码进行重度重构, 优化底层建表脚本; +- 3、执行器中任务线程分组逻辑优化: 之前根据执行器JobHandler进行线程分组,当多个任务复用Jobhanlder会导致相互阻塞。现改为根据调度中心任务进行任务线程分组,任务与任务执行相互隔离; +- 4、执行器调度通讯方案优化, 通过Hex + HC实现建议RPC通讯协议, 优化了通讯参数的维护和解析流程; +- 5、调度中心, 新建/编辑任务, 界面属性调整: + - 5.1、任务新增/编辑界面中去除 "任务名JobName"属性 ,该属性改为系统自动生成: 该字段之前主要用于在 "调度中心" 唯一标示一个任务, 现实意义不大, 因此计划淡化掉该字段,改为系统生成UUID,从而简化任务新建的操作; + - 5.2、任务新增/编辑界面中去除 "GLUE模式" 复选框位置调整, 改为贴近"JobHandler"输入框右侧; + - 5.3、任务新增/编辑界面中去除 "报警阈值" 属性; + - 5.4、任务新增/编辑界面中去除 "子任务Key" 属性, 每个任务全局任务Key可以从任务列表获取, 当本任务执行结束且成功后, 将会根据子任务Key匹配子任务并主动触发一次子任务执行; +- 6、问题修复: + - 6.1、执行器jetty关闭优化,解决一处可能导致jetty无法关闭的问题; + - 6.2、执行器任务终止时,执行队列回调优化,解决一处导致任务无法回调的问题; + - 6.3、调度中心中列表分页参数优化,解决一处因服务器限制post长度而引起的问题; + - 6.4、执行器Jobhandler注解优化,解决一处因事务代理导致的容器无法加载JobHandler的问题; + - 6.5、远程调度优化,禁用retry策略,解决一处可能导致重复调用的问题; + +Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段, 地址见分支 [V1.3](https://github.com/xuxueli/xxl-job/tree/v1.3) 。新特性将会在master分支持续更新。 + +### 7.7 版本 V1.4.1 新特性[2016-09-06] +- 1、项目成功推送maven中央仓库, 中央仓库地址以及依赖如下: + ``` + <!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ --> + <dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>${最新稳定版}</version> + </dependency> + ``` +- 2、为适配中央仓库规则, 项目groupId从com.xxl改为com.xuxueli。 +- 3、系统版本不在维护在项目跟pom中,各个子模块单独配置版本配置,解决子模块无法单独编译的问题; +- 4、底层RPC通讯,传输数据的字节长度统计规则优化,可节省50%数据传输量; +- 5、IJobHandler取消任务返回值,原通过返回值判断执行状态,逻辑改为:默认任务执行成功,仅在捕获异常时认定任务执行失败。 +- 6、系统公共弹框功能,插件化; +- 7、底层表结构,表明统一大写; +- 8、调度中心,异常处理器JSON响应的ContentType修改,修复浏览器不识别的问题; + +### 7.8 版本 V1.4.2 新特性[2016-09-29] +- 1、推送新版本 V1.4.2 至中央仓库, 大版本 V1.4 进入维护阶段; +- 2、任务新增时,任务列表偏移问题修复; +- 3、修复一处因bootstrap不支持模态框重叠而导致的样式错乱的问题, 在任务编辑时会出现该问题; +- 4、调度超时和Handler匹配不到时,调度状态优化; +- 5、因catch异常,导致任务不可终止的问题,给出解决方案, 见文档; + +### 7.9 版本 V1.5.0 特性[2016-11-13] +- 1、任务注册: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。 +- 2、"执行器" 新增参数 "AppName" : 是每个执行器集群的唯一标示AppName, 并周期性以AppName为对象进行自动注册。 +- 3、调度中心新增栏目 "执行器管理" : 管理在线的执行器, 通过属性AppName自动发现注册的执行器。只有被管理的执行器才允许被使用; +- 4、"任务组"属性改为"执行器": 每个任务需要绑定指定的执行器, 调度地址通过绑定的执行器获取; +- 5、抛弃"任务机器"属性: 通过任务绑定的执行器, 自动发现注册的远程执行器地址并触发调度请求。 +- 6、"公共依赖"中新增DBGlueLoader,基于原生jdbc实现GLUE源码的加载器,减少第三方依赖(mybatis,spring-orm等);精简和优化执行器测配置(针对GLUE任务),降低上手难度; +- 7、表结构调整,底层重构优化; +- 8、"调度中心"自动注册和发现,failover: 调度中心周期性自动注册, 任务回调时可以感知在线的所有调度中心地址, 通过failover的方式进行任务回调,避免回调单点风险。 + +### 7.10 版本 V1.5.1 特性[2016-11-13] +- 1、底层代码重构和逻辑优化,POM清理以及CleanCode; +- 2、Servlet/JSP Spec设定为3.0/2.2 +- 3、Spring升级至3.2.17.RELEASE版本; +- 4、Jetty升级版本至8.2.0.v20160908; +- 5、已推送V1.5.0和V1.5.1至Maven中央仓库; + +### 7.11 版本 V1.5.2 特性[2017-02-28] +- 1、IP工具类获取IP逻辑优化,IP静态缓存; +- 2、执行器、调度中心,均支持自定义注册IP地址;解决机器多网卡时错误网卡注册的情况; +- 3、任务跨天执行时生成多份日志文件的问题修复; +- 4、底层日志底层日志调整,非敏感日志level调整为debug; +- 5、升级数据库连接池c3p0版本; +- 6、执行器log4j配置优化,去除无效属性; +- 7、底层代码重构和逻辑优化以及CleanCode; +- 8、GLUE依赖注入逻辑优化,支持别名注入; + +### 7.12 版本 V1.6.0 特性[2017-03-13] +- 1、通讯方案升级,原基于HEX的通讯模型调整为基于HTTP的B-RPC的通讯模型; +- 2、执行器支持手动设置执行地址列表,提供开关切换使用注册地址还是手动设置的地址; +- 3、执行器路由规则:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移; +- 4、规范线程模型统一,统一线程销毁方案(通过listener或stop方法,容器销毁时销毁线程;Daemon方式有时不太理想); +- 5、规范系统配置数据,通过配置文件统一管理; +- 6、CleanCode,清理无效的历史参数; +- 7、底层扩展数据结构以及相关表结构调整; +- 8、新建任务默认为非运行状态; +- 9、GLUE模式任务实例更新逻辑优化,原根据超时时间更新改为根据版本号更新,源码变动版本号加一; + +### 7.13 版本 V1.6.1 特性[2017-03-25] +- 1、Rolling日志; +- 2、WebIDE交互重构; +- 3、通讯增强校验,有效过滤非正常请求; +- 4、权限增强校验,采用动态登录TOKEN(推荐接入内部SSO); +- 5、数据库配置优化,解决乱码问题; + +### 7.14 版本 V1.6.2 特性[2017-04-25] +- 1、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等; +- 2、JobHandler支持设置任务返回值,在任务逻辑中可以方便的控制任务执行结果; +- 3、资源路径包含空格或中文时资源文件无法加载时,无法准确查看异常信息的问题处理。 +- 4、路由策越优化:循环和LFU路由策略计数器自增无上限问题和首次路由压力集中在首台机器的问题修复; + +### 7.15 版本 V1.7.0 特性[2017-05-02] +- 1、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python和Groovy等类型脚本; +- 2、新增spring-boot类型执行器example项目; +- 3、升级jetty版本至9.2; +- 4、任务运行日志移除log4j组件依赖,改为底层自主实现,从而取消了对日志组件的依赖限制; +- 5、执行器移除GlueLoader依赖,改为推送方式实现,从而GLUE源码加载不再依赖JDBC; +- 6、登录拦截Redirect时获取项目名,解决非根据目录发布时跳转404问题; + +### 7.16 版本 V1.7.1 特性[2017-05-08] +- 1、运行日志读写编码统一为UTF-8,解决windows环境下日志乱码问题; +- 2、通讯超时时间限定为10s,避免异常情况下调度线程占用; +- 3、执行器,server启动、销毁和注册逻辑调整; +- 4、JettyServer关闭逻辑优化,修复执行器无法正常关闭导致端口占用和频繁打印c3p0日志的问题; +- 5、JobHandler中开启子线程时,支持子线程输出执行日志并通过Rolling查看。 +- 6、任务日志清理功能; +- 7、弹框组件统一替换为layer; +- 8、升级quartz版本至2.3.0; + +### 7.17 版本 V1.7.2 特性[2017-05-17] +- 1、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; +- 2、失败处理策略;调度失败时的处理策略,策略包括:失败告警(默认)、失败重试; +- 3、通讯时间戳超时时间调整为180s; +- 4、执行器与数据库彻底解耦,但是执行器需要配置调度中心集群地址。调度中心提供API供执行器回调和心跳注册服务,取消调度中心内部jetty,心跳周期调整为30s,心跳失效为三倍心跳; +- 5、执行参数编辑时丢失问题修复; +- 6、新增任务测试Demo,方便在开发时进行任务逻辑测试; + +### 7.18 版本 V1.8.0 特性[2017-07-17] +- 1、任务Cron更新逻辑优化,改为rescheduleJob,同时防止cron重复设置; +- 2、API回调服务失败状态码优化,方便问题排查; +- 3、XxlJobLogger的日志多参数支持; +- 4、路由策略新增 "忙碌转移" 模式:按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; +- 5、路由策略代码重构; +- 6、执行器重复注册问题修复; +- 7、任务线程轮空30次后自动销毁,降低低频任务的无效线程消耗。 +- 8、执行器任务执行结果批量回调,降低回调频率提升执行器性能; +- 9、springboot版本执行器,取消XML配置,改为类配置方式; +- 10、执行日志,支持根据运行 "状态" 筛选日志; +- 11、调度中心任务注册检测逻辑优化; + +### 7.19 版本 V1.8.1 特性[2017-07-30] +- 1、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数处理分片任务; +- 2、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 +- 3、执行器JobHandler禁止命名冲突; +- 4、执行器集群地址列表进行自然排序; +- 5、调度中心,DAO层代码精简优化并且新增测试用例覆盖; +- 6、调度中心API服务改为自研RPC形式,统一底层通讯模型; +- 7、新增调度中心API服务测试Demo,方便在调度中心API扩展和测试; +- 8、任务列表页交互优化,更换执行器分组时自动刷新任务列表,新建任务时默认定位在当前执行器位置; +- 9、访问令牌(accessToken):为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯; +- 10、springboot版本执行器,升级至1.5.6.RELEASE版本; +- 11、统一maven依赖版本管理; + +### 7.20 版本 V1.8.2 特性[2017-09-04] +- 1、项目主页搭建:提供中英文文档:https://www.xuxueli.com/xxl-job +- 2、JFinal执行器Sample示例项目; +- 3、事件触发:除了"Cron方式"和"任务依赖方式"触发任务执行之外,支持基于事件的触发任务方式。调度中心提供触发任务单次执行的API服务,可根据业务事件灵活触发。 +- 4、执行器摘除:执行器销毁时,主动通知调度中心并摘除对应执行器节点,提高执行器状态感知的时效性。 +- 5、执行器手动设置IP时将会绑定Host; +- 6、规范项目目录,方便扩展多执行器; +- 7、解决执行器回调URL不支持配置HTTPS时问题; +- 8、执行器回调线程销毁前, 批量回调队列中数据,防止任务结果丢失; +- 9、调度中心任务监控线程销毁时,批量对失败任务告警,防止告警信息丢失; +- 10、任务日志文件路径时间戳格式化时SimpleDateFormat并发问题解决; + +### 7.21 版本 V1.9.0 特性[2017-12-29] +- 1、新增Nutz执行器Sample示例项目; +- 2、新增任务运行模式 "GLUE模式(NodeJS) ",支持NodeJS脚本任务; +- 3、脚本任务Shell、Python和Nodejs等支持获取分片参数; +- 4、失败重试,完整支持:调度中心调度失败且启用"失败重试"策略时,将会自动重试一次;执行器执行失败且回调失败重试状态(新增失败重试状态返回值)时,也将会自动重试一次; +- 5、失败告警策略扩展:默认提供邮件失败告警,可扩展短信等,扩展代码位置为 "JobFailMonitorHelper.failAlarm"; +- 6、执行器端口支持自动生成(小于等于0时),避免端口定义冲突; +- 7、调度报表优化,支持时间区间筛选; +- 8、Log组件支持输出异常栈信息,底层实现优化; +- 9、告警邮件样式优化,调整为表格形式,邮件组件调整为commons-email简化邮件操作; +- 10、项目依赖全量升级至较新稳定版本,如spring、jackson等等; +- 11、任务日志,记录发起调度的机器信息; +- 12、交互优化,如登录注销; +- 13、任务Cron长度扩展支持至128位,支持负责类型Cron设置; +- 14、执行器地址录入交互优化,地址长度扩展支持至512位,支持大规模执行器集群配置; +- 15、任务参数“IJobHandler.execute”入参改为“String params”,增强入参通用性。 +- 16、IJobHandler提供init/destroy方法,支持在相应任务线程初始化和销毁时进行附加操作; +- 17、任务注解调整为 “@JobHandler”,与任务抽象接口统一; +- 18、修复任务监控线程被耗时任务阻塞的问题; +- 19、修复任务监控线程无法监控任务触发和执行状态均未0的问题; +- 20、执行器动态代理对象,拦截非业务方法的执行; +- 21、修复JobThread捕获Error错误不更新JobLog的问题; +- 22、修复任务列表界面左侧菜单合并时样式错乱问题; +- 23、调度中心项目日志配置改为xml文件格式; +- 24、Log地址格式兼容,支持非"/"结尾路径配置; +- 25、底层系统日志级别规范调整,清理遗留代码; +- 26、建表SQL优化,支持同步创建制定编码的库和表; +- 27、系统安全性优化,登录Token写Cookie时进行MD5加密,同时Cookie启用HttpOnly; +- 28、新增"任务ID"属性,移除"JobKey"属性,前者承担所有功能,方便后续增强任务依赖功能。 +- 29、任务循环依赖问题修复,避免子任务与父任务重复导致的调度死循环; +- 30、任务列表新增筛选条件 "任务描述",快速检索任务; +- 31、执行器Log文件定期清理功能:执行器新增配置项("xxl.job.executor.logretentiondays")日志保存天数,日志文件过期自动删除。 + +### 7.22 版本 V1.9.1 特性[2018-02-22] +- 1、国际化:调度中心实现国际化,支持中文、英文两种语言,默认为中文。 +- 2、调度报表新增"运行中"中状态项; +- 3、调度报表优化,报表SQL调优并且新增LocalCache缓存(缓存时间60s),提高大数据量下报表加载速度; +- 4、修复打包部署时资源文件乱码问题; +- 5、修复新版本chrome滚动到顶部失效问题; +- 6、调度中心配置加载优化,取消对配置文件名的强依赖,支持加载磁盘配置; +- 7、修复脚本任务Log文件未正常close的问题; +- 8、项目依赖全量升级至较新稳定版本,如spring、jackson等等; + +### 7.23 版本 V1.9.2 特性[2018-10-05] +- 1、任务超时控制:新增任务属性 "任务超时时间",并支持自定义,任务运行超时将会主动中断任务; +- 2、任务失败重试次数:新增任务属性 "失败重试次数",并支持自定义,当任务失败时将会按照预设的失败重试次数主动进行重试;同时收敛废弃其他失败重试策略,如调度失败、执行失败、状态码失败等; +- 3、新增任务运行模式 "GLUE模式(PHP) ",支持php脚本任务; +- 4、新增任务运行模式 "GLUE模式(PowerShell) ",支持PowerShell脚本任务; +- 5、调度全异步处理:任务触发之后,推送到调度队列,多线程并发处理调度请求,提高任务调度速率的同时,避免因网络问题导致quartz调度线程阻塞的问题; +- 6、执行器任务结果落盘优化:执行器回调失败时将任务结果写磁盘,待重启或网络恢复时重试回调任务结果,防止任务执行结果丢失; +- 7、任务日志查询速度大幅提升:百万级别数据量搜索速度提升1000倍; +- 8、调度中心提供API服务,支持通过API服务对任务进行查询、新增、更新、启停等操作; +- 9、底层自研Log组件参数占位符改为"{}",并修复打印有参日志时参数不匹配导致报错的问题; +- 10、任务回调结果优化,支持展示在Rolling log中,方便问题排查; +- 11、底层LocalCache组件兼容性优化,支持jdk9、jdk10及以上版本编译部署; +- 12、告警邮件固定使用 UTF-8 编码格式,修复由机器编码导致的邮件乱码问题; +- 13、告警邮件中展示失败告警信息; +- 14、告警邮箱支持SSL配置; +- 15、Window机器下File.separator不兼容问题修复; +- 16、脚本任务异常Log输出优化; +- 17、任务线程停止变量修饰符优化; +- 18、脚本任务Log文件流关闭优化; +- 19、任务报表成功、失败和进行中统计问题修复; +- 20、核心依赖Core内部国际化处理; +- 21、默认Quartz线程数调整为50; +- 22、新增左侧菜单"运行报表"; +- 23、执行器手动设置IP时取消绑定Host的操作,该IP仅供执行器注册使用;修复指定外网IP时无法绑定执行器Host的问题; +- 24、取消父子任务不可重复的限制,支持循环任务触发等特殊场景; +- 25、任务调度备注中标注任务触发类型,如Cron触发、父任务触发、API触发等等,方便排查调度日志; +- 26、底层日志组件SimpleDateFormat线程安全问题修复; +- 27、执行器通讯线程优化,corePoolSize从256降低至32; +- 28、任务日志表状态字段类型优化; +- 29、GLUE脚本文件自动清理功能,及时清理过期脚本文件; +- 30、执行器注册方式切换优化,切换自动注册时主动同步在线机器,避免执行器为空的问题; +- 31、跨平台:除了提供Java、Python、PHP等十来种任务模式之外,新增提供基于HTTP的任务模式; +- 32、底层RPC序列化协议调整为hessian2; +- 33、修复表字段 “t.order”与数据库关键字冲突查询失败的问题, +- 34、任务属性枚举 "任务模式、阻塞策略" 国际化优化; +- 35、分片任务失败重试优化,仅重试当前失败的分片; +- 36、任务触发时支持动态传参,调度中心与API服务均提供提供动态参数功能; +- 37、任务执行日志、调度日志字段类型调整,改为text类型并取消字数限制; +- 38、GLUE任务脚本字段类型调整,改为mediumtext类型,提高GLUE长度上限; +- 39、任务监控线程Log输出优化,运行中任务的监控Log改为debug级别,减少非核心日志量; +- 40、项目依赖全量升级至较新稳定版本,如spring、Jackson、groovy等等; +- 41、docker支持:调度中心提供 Dockerfile 方便快速构建docker镜像; + +### 7.24 版本 V2.0.0 Release Notes[2018-11-04] +- 1、调度中心迁移到 springboot; +- 2、底层通讯组件迁移至 xxl-rpc; +- 3、容器化:提供官方docker镜像,并实时更新推送dockerhub(docker pull xuxueli/xxl-job-admin),进一步实现产品开箱即用; +- 4、新增无框架执行器Sample示例项目 "xxl-job-executor-sample-frameless"。不依赖第三方框架,只需main方法即可启动运行执行器; +- 5、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +- 6、任务状态优化,仅运行状态"NORMAL"任务关联至quartz,降低quartz底层数据存储与调度压力; +- 7、任务状态规范:新增任务默认停止状态,任务更新时保持任务状态不变; +- 8、IP获取逻辑优化,优先遍历网卡来获取可用IP; +- 9、任务新增的API服务接口返回任务ID,方便调用方实用; +- 10、组件化优化,移除对 spring 的依赖:非spring应用选用 "XxlJobExecutor" 、spring应用选用 "XxlJobSpringExecutor" 作为执行器组件; +- 11、任务RollingLog展示逻辑优化,修复超时任务无法查看的问题; +- 12、多项UI组件升级到最新版本,如:CodeMirror、Echarts、Jquery 等; +- 13、项目依赖升级 groovy 至较新稳定版本;pom清理; +- 14、子任务失败重试重试逻辑优化,子任务失败时将会按照其预设的失败重试次数主动进行重试 + +### 7.25 版本 v2.0.1 Release Notes[2018-11-09] +- 1、左侧菜单折叠动画问题修复; +- 2、调度报表日期分布图默认值统一; +- 3、freemarker对数字默认加千分位问题修复,解决日志ID被分隔导致查看日志失败问题; +- 4、底层通讯组件升级,修复通讯异常时无效等待的问题; +- 5、执行器启动之后jetty停止的问题修复; + +### 7.26 版本 v2.0.2 Release Notes[2019-04-20] +- 1、底层通讯方案优化:升级较新版本xxl-rpc,由"JETTY"方案调整为"NETTY_HTTP"方案,执行器内嵌netty-http-server提供服务,调度中心复用容器端口提供服务; +- 2、任务告警逻辑调整,改为通过扫描失败日志方式触发。一方面精确扫描失败任务,降低扫描范围;另一方面取消内存队列,降低线程内存消耗; +- 3、Quartz触发线程池废弃并替换为 "XxlJobThreadPool",降低线程切换、内存占用带来的消耗,提高调度性能; +- 4、调度线程池隔离,拆分为"Fast"和"Slow"两个线程池,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; +- 5、执行器热部署时JobHandler重新初始化,修复由此导致的 "jobhandler naming conflicts." 问题; +- 6、新增Class的加载缓存,解决频繁加载Class会使jvm的方法区空间不足导致OOM的问题; +- 7、任务支持更换绑定执行器,方便任务分组转移和管理; +- 8、调度中心告警邮件发送组件改为 “spring-boot-starter-mail”; +- 9、记住密码功能优化,选中时永久记住;非选中时关闭浏览器即登出; +- 10、项目依赖升级至较新稳定版本,如quartz、spring、jackson、groovy、xxl-rpc等等; +- 11、精简项目,取消第三方依赖,如 commons-collections4、commons-lang3 ; +- 12、执行器回调日志落盘方案复用RPC序列化方案,并移除Jackson依赖; +- 13、底层Log调优,应用正常终止取消异常栈信息打印; +- 14、交互优化,尽量避免新开页面窗口;仅WebIDE支持新开页,并提供窗口快速关闭按钮;任务启、停、删除、触发等轻操作提示改为toast方式, +- 15、任务暂停、删除优化,避免quartz delete不完整导致任务脏数据; +- 16、任务回调、心跳注册成功日志优化,非核心常规日志调整为debug级别,降低冗余日志输出; +- 17、调整首页报表默认区间为本周,避免日志量太大查询缓慢; +- 18、LRU路由更新不及时问题修复; +- 19、任务失败告警邮件发送逻辑优化; +- 20、调度日志排序逻辑调整为按照调度时间倒序,兼容TIDB等主键不连续日志存储组件; +- 21、执行器优雅停机优化; +- 22、连接池配置优化,增强连接有效性验证; +- 23、JobHandler#msg长度限制,修复异常情况下日志超长导致内存溢出的问题; +- 24、升级xxl-rpc至较新版本,修复springboot 2.x版本兼容性问题; + +### 7.27 版本 v2.1.0 Release Notes[2019-07-07] +- 1、自研调度组件,移除quartz依赖:一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性; + - 触发:单节点周期性触发,运行事件如delayqueue; + - 调度:集群竞争,负载方式协同处理,锁竞争-更新触发信息-推送时间轮-锁释放-锁竞争; +- 2、底层表结构重构:移除11张quartz相关表,并对现有表结构优化梳理; +- 3、任务日志主键调整为long数据类型,防止海量日志情况下数据溢出; +- 4、底层线程模型重构:移除Quartz线程池,降低系统线程与内存开销; +- 5、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; +- 6、权限管理:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; +- 7、调度线程池参数调优; +- 8、注册表索引优化,缓解锁表问题; +- 9、新增Jboot执行器Sample示例项目; +- 10、任务列表优化,支持根据 "任务状态"、"负责人" 属性筛选任务; +- 11、任务日志列表交互优化,操作按钮合并为分割按钮; +- 12、项目依赖升级至较新稳定版本,如spring、springboot、groovy、xxl-rpc等等;并清理冗余POM; +- 13、升级xxl-rpc至较新版本,修复代理服务初始化时远程服务不可用导致长连冗余创建的问题; +- 14、首页调度报表的日期排序在TIDB下乱序问题修复; +- 15、调度中心与执行器双向通讯超时时间调整为3s; +- 16、调度组件销毁流程优化,先停止调度线程,然后等待时间轮内存量任务处理完成,最终销毁时间轮线程; +- 17、执行器回调线程优化,回调地址为空时销毁问题修复; +- 18、HttpJobHandler优化,响应数据指定UTF-8格式,避免中文乱码; +- 19、代码优化,ConcurrentHashMap变量类型改为ConcurrentMap,避免因不同版本实现不同导致的兼容性问题; + +### 7.28 版本 v2.1.1 Release Notes[2019-11-24] +- 1、 调度中心日志自动清理功能(至此,调度中心/执行器均支持日志自动清理,过期天数均默认设置为30天):调度中心新增配置项("xxl.job.logretentiondays")日志保存天数,过期日志自动清理;解决海量日志情况下日志表慢SQL问题;限制大于等于7时生效,否则关闭清理功能,默认为30; +- 2、 调度报表优化:新增日志报表的存储表,三天内的任务日志会以每分钟一次的频率异步同步至报表中;任务报表仅读取报表数据,极大提升加载速度; +- 3、 Cron在线生成工具:任务新增、编辑框通过组件在线生成Cron表达式; +- 4、 Cron下次执行时间查询:支持通过界面在线查看后续连续5次执行时间; +- 5、 调度中心新增应用健康检查功能,借助“spring-boot-starter-actuator”,相对地址 “/actuator/health”; +- 6、 DB脚本默认编码改为utf8mb4,修复字符乱码问题(建议Mysql版本5.7+); +- 7、 调度中心任务平均分配,触发组件每次获取与线程池数量相关数量的任务,避免大量任务集中在单个调度中心集群节点; +- 8、 任务触发组件优化,预加载频率正常1s一次,当预加载轮空时主动休眠一个加载周期,动态降低加载频率从而降低DB压力; +- 9、 调度组件优化:针对永远不会触发的Cron禁止配置和启动;任务Cron最后一次触发后再也不会触发时,比如一次性任务,主动停止相关任务; +- 10、DB重连优化,修复DB宕机重连后任务调度停止的问题,重连后自动加入调度集群触发任务调度; +- 11、注册监控线程优化,降低死锁几率; +- 12、调度中心日志删除优化,改为分页获取ID并根据ID删除的方式,避免批量删除海量日志导致死锁问题; +- 13、任务重试时参数丢失的问题修复; +- 14、调度中心移除SQL中的 "now()" 函数;集群部署时不再依赖DB时钟,仅需要保证调度中心应用节点时钟一致即可; +- 15、任务触发组件加载顺序调整,避免小概率情况下组件随机加载顺序导致的I18N的NPE问题; +- 16、JobThread自销毁优化,避免并发触发导致triggerQueue中任务丢失问题; +- 17、调度中心密码限制18位,修复修改密码超过18位无法登录的问题; +- 18、任务告警组件分页参数无效问题修复; +- 19、升级xxl-rpc版本:服务端线程优化,降低线程内存开销;IpUtil优化:增加连通性校,过滤明确非法的网卡; +- 20、调度中心回调API服务改为restful方式; +- 21、UI优化,任务列表和日志列表数据表格宽度比例调整,避免数据换行提升体验; +- 22、登录界面取消默认填写的登录账号密码; +- 23、执行器表属性调整,"顺序" 属性调整为整型,解决执行器数据较多时无法正确排序的问题; +- 24、任务列表交互优化,支持查看任务所属执行器的注册节点; +- 25、项目依赖升级至较新稳定版本,如spring、spring-boot、mybatis、slf4j、groovy等等; +- 26、日志组件优化:调度中心支持控制每次请求最大加载行数,日志量太大时分批请求,避免单次加载日志量太大阻塞页面; + +### 7.29 版本 v2.1.2 Release Notes[2019-12-12] +- 1、方法任务支持:由原来基于JobHandler类任务开发方式,优化为支持基于方法的任务开发方式;因此,可以支持单个类中开发多个任务方法,进行类复用 +``` +@XxlJob("demoJobHandler") +public ReturnT<String> execute(String param) { + XxlJobLogger.log("hello world"); + return ReturnT.SUCCESS; +} +``` +- 2、移除commons-exec,采用原生方式实现,降低第三方依赖; +- 3、执行器回调乱码问题修复; +- 4、调度中心dispatcher servlet加载顺序优化; +- 5、执行器回调地址https兼容支持; +- 6、多个项目依赖升级至较新稳定版本; +- 注意:最新版本 "XxlJobSpringExecutor" 逻辑有调整,历史项目中该组件的配置方式请参考Sample示例项目进行调整,尤其注意需要移除组件的init和destroy方法; + +### 7.30 版本 v2.2.0 Release Notes[2020-04-14] +- 1、RESTful API:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。 +- 2、任务复制功能:点击复制是弹出新建任务弹框,并初始化被复制任务信息; +- 3、任务手动执行一次的时候,支持指定本次执行的机器地址,为空则从执行器获取; +- 4、任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; +- 5、调度中心升级springboot2.x;因此,系统要求JDK8+; +- 6、XxlJob注解扫描方式优化,支持查找父类以及接口和基于类代理等常见情况;修复任务为空时小概率NPE问题; +- 7、移除旧类注解JobHandler,推荐使用基于方法注解 "@XxlJob" 的方式进行任务开发;(如需保留类注解JobHandler使用方式,可以参考旧版逻辑定制开发); +- 8、任务告警组件模块化:如果需要新增一种告警方式,只需要新增一个实现 "com.xxl.job.admin.core.alarm.JobAlarm" 接口的告警实现即可,更加灵活、方便定制; +- 9、调度中心国际化完善:新增 "中文繁体" 支持。默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文; +- 10、执行器注册逻辑优化:新增配置项 ”注册地址 / xxl.job.executor.address“,优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。 +- 11、默认数据库连接池调整为hikari,移除tomcat-jdbc依赖; +- 12、多个项目依赖升级至较新稳定版本,如mybatis、groovy和mysql驱动等; +- 13、执行器优雅停机优化,修复任务线程中断未join导致回调丢失的问题; +- 14、一致性哈希路由策略优化:默认虚拟节点数量调整为100,提高路由的均衡性; +- 15、通用HTTP任务Handler(httpJobHandler)优化,扩展自定义参数信息,示例参数如下; +``` +url: http://www.xxx.com +method: get 或 post +data: post-data +``` +- 16、SQL脚本编码默认utf8mb4执行,避免小概率下容器环境中乱码问题; +- 17、Web IDE交互问题修复:输入源码备注之后按回车跳转error问题处理; +- 18、执行器初始化逻辑优化:修复懒加载的Bean被提前初始化问题; +- 19、执行器注册默认值优化; +- 20、修复bootstrap.min.css.map 404问题; +- 21、执行器UI交互优化,移除冗余order属性; +- 22、执行备注消息长度限制,修复数据超长无法存储导致导致回调失败的问题; +注意:XxlJobSpringExecutor组件个别字段调整:“appName” 调整为 “appname” ,升级时该组件时需要注意; + +### 7.31 版本 v2.3.0 Release Notes[2021-02-09] +- 1、【新增】调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等; +- 2、【新增】触发策略:除了常规Cron、API、父子任务触发方式外,新增提供 "固定间隔触发、(固定延时触发,实验中)" 新触发方式; +- 3、【新增】新增任务辅助工具 "XxlJobHelper":提供统一任务辅助能力,包括:任务上下文信息维护获取(任务参数、任务ID、分片参数)、日志输出、任务结果设置……等; + - 3.1、"ShardingUtil" 组件废弃:改用 "XxlJobHelper.getShardIndex()/getShardTotal();" 获取分片参数; + - 3.2、"XxlJobLogger" 组件废弃:改用 "XxlJobHelper.log" 进行日志输出; +- 4、【优化】任务核心类 "IJobHandler" 的 "execute" 方法取消出入参设计。改为通过 "XxlJobHelper.getJobParam" 获取任务参数并替代方法入参,通过 "XxlJobHelper.handleSuccess/handleFail" 设置任务结果并替代方法出参,示例代码如下; +``` +@XxlJob("demoJobHandler") +public void execute() { + String param = XxlJobHelper.getJobParam(); // 获取参数 + XxlJobHelper.handleSuccess(); // 设置任务结果 +} +``` +- 5、【优化】Cron编辑器增强:Cron编辑器修改cron时可实时查看最近运行时间; +- 6、【优化】执行器示例项目规范整理; +- 7、【优化】任务调度生命周期重构:调度(schedule)、触发(trigger)、执行(handle)、回调(callback)、结束(complete); +- 8、【优化】执行器注册组件优化:注册逻辑调整为异步方式,提高注册性能; +- 9、【优化】执行器鉴权校验:执行器启动时主动校验accessToken,为空则主动Warn告警;(已规划安全强化:AccessToken动态生成、动态启停等) +- 10、【优化】邮箱告警配置优化:将"spring.mail.from"与"spring.mail.username"属性拆分开,更加灵活的支持一些无密码邮箱服务; +- 11、【优化】多个项目依赖升级至较新稳定版本,如netty、groovy、spring、springboot、mybatis等; +- 12、【优化】UI组件常规升级,提升组件稳定性; +- 13、【优化】调度中心页面交互优化:用户管理模块密码列取消;多处表达autocomplete取消;执行器管理模块XSS拦截校验等; +- 14、【优化】调度中心任务状态探测慢SQL问题优化; +- 15、【修复】GLUE-Java模式任务,init/destroy无法执行问题修复; +- 16、【修复】Cron编辑器问题修复:修复小概率情况下cron单个字段修改时导致其他字段被重置问题; +- 17、【修复】通用HTTP任务Handler(httpJobHandler)优化:修复 "setDoOutput(true)" 导致任务请求GetMethod失效问题; +- 18、【修复】执行器Commandhandler示例任务优化,修复极端情况下脚本进程挂起问题; +- 19、【修复】调度通讯组件优化,修复RestFul方式调用 DotNet 版本执行器时心跳检测失败问题; +- 20、【修复】调度中心远程执行日志查询乱码问题修复; +- 21、【修复】调度中心组件加载顺序优化,修复极端情况下调度组件初始慢导致的调度失败问题; +- 22、【修复】执行器注册线程优化,修复极端情况下初始化失败时导致NPE问题; +- 23、【修复】调度线程连接池优化,修复连接有效性校验超时问题; +- 24、【修复】执行器注册表字段优化,解决执行器注册节点过多导致注册信息存储和更新失败的问题; +- 25、【修复】轮训路由策略优化,修复小概率下并发问题; +- 26、【修复】页面redirect跳转后https变为http问题修复; +- 27、【修复】执行器日志清理优化,修复小概率下日志文件为空导致清理异常问题; + +### 7.32 版本 v2.3.1 Release Notes[2022-05-21] +- 1、【修复】修复风险漏洞,升级问题低版本项目依赖:CVE-2021-2471、CVE-2022-22965等。 +- 2、【修复】修复故障告警逻辑,邮箱校验逻辑下放至EmailJobAlarm中,避免对其他告警方式的干扰。 +- 3、【优化】调度通讯默认启用accessToken,提升系统安全性(建议生产环境自定义accessToken)。 +- 4、【优化】合并多项PR,项目代码结构、健壮性优化:PR-2833、PR-2812、PR-2541、PR-2537、PR-2514、PR-2509、PR-2591。 +- 5、【优化】任务线程名优化,提升可读性与问题定位效率(ISSUE-2527)。 + +### 7.33 版本 v2.4.0 Release Notes[规划中] +- 1、【优化】[规划中]任务日志重构:一次调度只记录一条主任务,维护起止时间和状态。 + - 普通任务:只记录一条主任务; + - 广播任务:记录一条主任务,每个分片任务记录一条次任务,关联在主任务上; + - 重试任务:失败时,新增主任务。所有调度记录,包括入口调度和重试调度,均挂载主任务上。 +- 2、【优化】[规划中]分片任务:全部完成后才会出发后置节点; + +### 7.34 版本 v2.4.1 Release Notes[规划中] +- 1、[规划中]DAG流程任务 + - DAG任务:支持参数传递,共享数据:DAG任务创建、管理,DAG任务日志查看、操作; + - 子任务:废弃 +- 2、[规划中]多数据库支持,DAO层通过JPA实现,不限制数据库类型; +- 3、[规划中]告警增强:邮件告警 + webhook告警; +- 4、[规划中]安全强化:AccessToken动态生成、动态启停;控制调度、回调; +- 5、[规划中]任务导入导出工具,灵活支持版本升级、迁移等场景。 + +### TODO LIST +- 1、任务分片路由:分片采用一致性Hash算法计算出尽量稳定的分片顺序,即使注册机器存在波动也不会引起分批分片顺序大的波动;目前采用IP自然排序,可以满足需求,待定; +- 2、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。 +- 3、调度任务优先级; +- 4、多数据库支持,DAO层通过JPA实现,不限制数据库类型; +- 5、执行器Log清理功能:调度中心Log删除时同步删除执行器中的Log文件; +- 6、延时任务:API触发,支持"动态传参、延时消费";该功能与 XXL-MQ 冲突,该场景建议用后者; +- 7、调度线程池改为协程方式实现,大幅降低系统内存消耗; +- 8、任务、执行器数据全量本地缓存;新增消息表广播通知; +- 9、忙碌转移优化,全部机器忙碌时不再直接失败; +- 10、任务触发参数优化:支持选择 "Cron触发"、"固定间隔时间触发"、"指定时间点触发"、"不选择" 等; +- 11、调度日志列表加上执行时长列,并支持排序; +- 12、DAG流程任务: + - 替换子任务,支持参数传递,共享数据: + - 配置并列的"a-b、b-c"路径列表,构成串行、并行、dag任务流程,"dagre-d3"绘图;任务依赖,流程图,子任务+会签任务,各节点日志;支持根据成功、失败选择分支; + - 分片任务:全部完成后才会出发后置节点; +- 13、日期过滤:支持多个时间段排除; +- 14、告警增强: + - 邮件告警:支持自定义标题、模板格式; + - webhook告警:支持自定义告警URL、请求体格式; +- 15、新增任务运行模式 "GLUE模式(GO) ",支持GO任务; +- 16、GLUE 模式 Web Ide 版本对比功能; +- 17、注册中心优化,实时性注册发现:心跳注册间隔10s,refresh失败则首次注册并立即更新注册信息,心跳类似;30s过期销毁; +- 18、提供执行器Docker镜像; +- 19、脚本任务,支持数据参数,新版本仅支持单参数不支持需要兼容; +- 20、批量调度:调度请求入queue,调度线程批量获取调度请求并发起远程调度;提高线程效率; +- 21、执行器端口复用,复用容器端口提供通讯服务; +- 22、分片任务全部成功后触发子任务; +- 23、新增执行器描述属性;任务名称属性; +- 24、自定义失败重试时间间隔; +- 25、任务标签:方便搜索; +- 26、执行器:dag执行器,不需要注册机器; + + +## 八、其他 + +### 8.1 项目贡献 +欢迎参与项目贡献!比如提交PR修复一个bug,或者新建 [Issue](https://github.com/xuxueli/xxl-job/issues/) 讨论新特性或者变更。 + +### 8.2 用户接入登记 +更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 + +### 8.3 开源协议和版权 +产品开源免费,并且将持续提供免费的社区技术支持。个人或企业内部可自由的接入和使用。如有需要可邮件联系作者免费获取项目授权。 + +- Licensed under the GNU General Public License (GPL) v3. +- Copyright (c) 2015-present, xuxueli. + +--- +### 捐赠 +无论捐赠金额多少都足够表达您这份心意,非常感谢 :) [前往捐赠](https://www.xuxueli.com/page/donate.html ) diff --git "a/xxl-job/doc/XXL-JOB\346\236\266\346\236\204\345\233\276.pptx" "b/xxl-job/doc/XXL-JOB\346\236\266\346\236\204\345\233\276.pptx" new file mode 100644 index 0000000..5445216 --- /dev/null +++ "b/xxl-job/doc/XXL-JOB\346\236\266\346\236\204\345\233\276.pptx" Binary files differ diff --git a/xxl-job/doc/db/tables_xxl_job.sql b/xxl-job/doc/db/tables_xxl_job.sql new file mode 100644 index 0000000..ed8ecae --- /dev/null +++ b/xxl-job/doc/db/tables_xxl_job.sql @@ -0,0 +1,122 @@ +# +# XXL-JOB v2.3.1 +# Copyright (c) 2015-present, xuxueli. + +CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci; +use `xxl_job`; + +SET NAMES utf8mb4; + +CREATE TABLE `xxl_job_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `job_group` int(11) NOT NULL COMMENT '执行器主键ID', + `job_desc` varchar(255) NOT NULL, + `add_time` datetime DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `author` varchar(64) DEFAULT NULL COMMENT '作者', + `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件', + `schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型', + `schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型', + `misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略', + `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略', + `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler', + `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数', + `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略', + `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒', + `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数', + `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型', + `glue_source` mediumtext COMMENT 'GLUE源代码', + `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注', + `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间', + `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔', + `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行', + `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间', + `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `job_group` int(11) NOT NULL COMMENT '执行器主键ID', + `job_id` int(11) NOT NULL COMMENT '任务,主键ID', + `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址', + `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler', + `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数', + `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2', + `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数', + `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间', + `trigger_code` int(11) NOT NULL COMMENT '调度-结果', + `trigger_msg` text COMMENT '调度-日志', + `handle_time` datetime DEFAULT NULL COMMENT '执行-时间', + `handle_code` int(11) NOT NULL COMMENT '执行-状态', + `handle_msg` text COMMENT '执行-日志', + `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败', + PRIMARY KEY (`id`), + KEY `I_trigger_time` (`trigger_time`), + KEY `I_handle_code` (`handle_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_log_report` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `trigger_day` datetime DEFAULT NULL COMMENT '调度-时间', + `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量', + `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量', + `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量', + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_logglue` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `job_id` int(11) NOT NULL COMMENT '任务,主键ID', + `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型', + `glue_source` mediumtext COMMENT 'GLUE源代码', + `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注', + `add_time` datetime DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_registry` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `registry_group` varchar(50) NOT NULL, + `registry_key` varchar(255) NOT NULL, + `registry_value` varchar(255) NOT NULL, + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_group` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `app_name` varchar(64) NOT NULL COMMENT '执行器AppName', + `title` varchar(12) NOT NULL COMMENT '执行器名称', + `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入', + `address_list` text COMMENT '执行器地址列表,多地址逗号分隔', + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL COMMENT '账号', + `password` varchar(50) NOT NULL COMMENT '密码', + `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员', + `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割', + PRIMARY KEY (`id`), + UNIQUE KEY `i_username` (`username`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_lock` ( + `lock_name` varchar(50) NOT NULL COMMENT '锁名称', + PRIMARY KEY (`lock_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' ); +INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', ''); +INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL); +INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock'); + +commit; + diff --git "a/xxl-job/doc/images/cnblog-\351\246\226\351\241\265-\346\257\217\346\227\245\344\270\200\345\215\232-\347\254\254\344\270\200.png" "b/xxl-job/doc/images/cnblog-\351\246\226\351\241\265-\346\257\217\346\227\245\344\270\200\345\215\232-\347\254\254\344\270\200.png" new file mode 100644 index 0000000..f292d3e --- /dev/null +++ "b/xxl-job/doc/images/cnblog-\351\246\226\351\241\265-\346\257\217\346\227\245\344\270\200\345\215\232-\347\254\254\344\270\200.png" Binary files differ diff --git "a/xxl-job/doc/images/cnblog-\351\246\226\351\241\265-\347\203\255\351\227\250\345\212\250\345\274\271-\347\254\254\344\270\200.png" "b/xxl-job/doc/images/cnblog-\351\246\226\351\241\265-\347\203\255\351\227\250\345\212\250\345\274\271-\347\254\254\344\270\200.png" new file mode 100644 index 0000000..a594c06 --- /dev/null +++ "b/xxl-job/doc/images/cnblog-\351\246\226\351\241\265-\347\203\255\351\227\250\345\212\250\345\274\271-\347\254\254\344\270\200.png" Binary files differ diff --git a/xxl-job/doc/images/donate-alipay.jpg b/xxl-job/doc/images/donate-alipay.jpg new file mode 100644 index 0000000..78472ce --- /dev/null +++ b/xxl-job/doc/images/donate-alipay.jpg Binary files differ diff --git a/xxl-job/doc/images/donate-paypal.png b/xxl-job/doc/images/donate-paypal.png new file mode 100644 index 0000000..24e78a4 --- /dev/null +++ b/xxl-job/doc/images/donate-paypal.png Binary files differ diff --git a/xxl-job/doc/images/donate-wechat.png b/xxl-job/doc/images/donate-wechat.png new file mode 100644 index 0000000..4d16dab --- /dev/null +++ b/xxl-job/doc/images/donate-wechat.png Binary files differ diff --git a/xxl-job/doc/images/gitee-gvp.jpg b/xxl-job/doc/images/gitee-gvp.jpg new file mode 100644 index 0000000..dcc195b --- /dev/null +++ b/xxl-job/doc/images/gitee-gvp.jpg Binary files differ diff --git a/xxl-job/doc/images/img_1001.png b/xxl-job/doc/images/img_1001.png new file mode 100644 index 0000000..549880b --- /dev/null +++ b/xxl-job/doc/images/img_1001.png Binary files differ diff --git a/xxl-job/doc/images/img_1002.png b/xxl-job/doc/images/img_1002.png new file mode 100644 index 0000000..411f8ec --- /dev/null +++ b/xxl-job/doc/images/img_1002.png Binary files differ diff --git a/xxl-job/doc/images/img_6yC0.png b/xxl-job/doc/images/img_6yC0.png new file mode 100644 index 0000000..01bf573 --- /dev/null +++ b/xxl-job/doc/images/img_6yC0.png Binary files differ diff --git a/xxl-job/doc/images/img_BPLG.png b/xxl-job/doc/images/img_BPLG.png new file mode 100644 index 0000000..c928205 --- /dev/null +++ b/xxl-job/doc/images/img_BPLG.png Binary files differ diff --git a/xxl-job/doc/images/img_EB65.png b/xxl-job/doc/images/img_EB65.png new file mode 100644 index 0000000..ccf222d --- /dev/null +++ b/xxl-job/doc/images/img_EB65.png Binary files differ diff --git a/xxl-job/doc/images/img_Fgql.png b/xxl-job/doc/images/img_Fgql.png new file mode 100644 index 0000000..f884051 --- /dev/null +++ b/xxl-job/doc/images/img_Fgql.png Binary files differ diff --git a/xxl-job/doc/images/img_Hr2T.png b/xxl-job/doc/images/img_Hr2T.png new file mode 100644 index 0000000..4b5a73c --- /dev/null +++ b/xxl-job/doc/images/img_Hr2T.png Binary files differ diff --git a/xxl-job/doc/images/img_Qohm.png b/xxl-job/doc/images/img_Qohm.png new file mode 100644 index 0000000..854b6c8 --- /dev/null +++ b/xxl-job/doc/images/img_Qohm.png Binary files differ diff --git a/xxl-job/doc/images/img_UDSo.png b/xxl-job/doc/images/img_UDSo.png new file mode 100644 index 0000000..a0b81a4 --- /dev/null +++ b/xxl-job/doc/images/img_UDSo.png Binary files differ diff --git a/xxl-job/doc/images/img_V3vF.png b/xxl-job/doc/images/img_V3vF.png new file mode 100644 index 0000000..4607c84 --- /dev/null +++ b/xxl-job/doc/images/img_V3vF.png Binary files differ diff --git a/xxl-job/doc/images/img_Wb2o.png b/xxl-job/doc/images/img_Wb2o.png new file mode 100644 index 0000000..fec8dca --- /dev/null +++ b/xxl-job/doc/images/img_Wb2o.png Binary files differ diff --git a/xxl-job/doc/images/img_Ypik.png b/xxl-job/doc/images/img_Ypik.png new file mode 100644 index 0000000..6b4a2dd --- /dev/null +++ b/xxl-job/doc/images/img_Ypik.png Binary files differ diff --git a/xxl-job/doc/images/img_Z9Qr.png b/xxl-job/doc/images/img_Z9Qr.png new file mode 100644 index 0000000..2bfb044 --- /dev/null +++ b/xxl-job/doc/images/img_Z9Qr.png Binary files differ diff --git a/xxl-job/doc/images/img_ZAhX.png b/xxl-job/doc/images/img_ZAhX.png new file mode 100644 index 0000000..4a6039a --- /dev/null +++ b/xxl-job/doc/images/img_ZAhX.png Binary files differ diff --git a/xxl-job/doc/images/img_ZAsz.png b/xxl-job/doc/images/img_ZAsz.png new file mode 100644 index 0000000..bbb83ec --- /dev/null +++ b/xxl-job/doc/images/img_ZAsz.png Binary files differ diff --git a/xxl-job/doc/images/img_dNUJ.png b/xxl-job/doc/images/img_dNUJ.png new file mode 100644 index 0000000..a1a57a0 --- /dev/null +++ b/xxl-job/doc/images/img_dNUJ.png Binary files differ diff --git a/xxl-job/doc/images/img_eYrv.png b/xxl-job/doc/images/img_eYrv.png new file mode 100644 index 0000000..3f9c5e9 --- /dev/null +++ b/xxl-job/doc/images/img_eYrv.png Binary files differ diff --git a/xxl-job/doc/images/img_hIci.png b/xxl-job/doc/images/img_hIci.png new file mode 100644 index 0000000..0529209 --- /dev/null +++ b/xxl-job/doc/images/img_hIci.png Binary files differ diff --git a/xxl-job/doc/images/img_iUw0.png b/xxl-job/doc/images/img_iUw0.png new file mode 100644 index 0000000..b746000 --- /dev/null +++ b/xxl-job/doc/images/img_iUw0.png Binary files differ diff --git a/xxl-job/doc/images/img_inc8.png b/xxl-job/doc/images/img_inc8.png new file mode 100644 index 0000000..e1d38d6 --- /dev/null +++ b/xxl-job/doc/images/img_inc8.png Binary files differ diff --git a/xxl-job/doc/images/img_jOAU.png b/xxl-job/doc/images/img_jOAU.png new file mode 100644 index 0000000..beddc97 --- /dev/null +++ b/xxl-job/doc/images/img_jOAU.png Binary files differ diff --git a/xxl-job/doc/images/img_jrdI.png b/xxl-job/doc/images/img_jrdI.png new file mode 100644 index 0000000..69b38c0 --- /dev/null +++ b/xxl-job/doc/images/img_jrdI.png Binary files differ diff --git a/xxl-job/doc/images/img_o8HQ.png b/xxl-job/doc/images/img_o8HQ.png new file mode 100644 index 0000000..46b5bd0 --- /dev/null +++ b/xxl-job/doc/images/img_o8HQ.png Binary files differ diff --git a/xxl-job/doc/images/img_tJOq.png b/xxl-job/doc/images/img_tJOq.png new file mode 100644 index 0000000..f3d0062 --- /dev/null +++ b/xxl-job/doc/images/img_tJOq.png Binary files differ diff --git a/xxl-job/doc/images/img_tvGI.png b/xxl-job/doc/images/img_tvGI.png new file mode 100644 index 0000000..4b2265a --- /dev/null +++ b/xxl-job/doc/images/img_tvGI.png Binary files differ diff --git "a/xxl-job/doc/images/qq\347\276\244-\344\270\200\344\270\252xxl\345\220\214\345\255\246\350\277\233\344\272\20658.png" "b/xxl-job/doc/images/qq\347\276\244-\344\270\200\344\270\252xxl\345\220\214\345\255\246\350\277\233\344\272\20658.png" new file mode 100644 index 0000000..a525e0b --- /dev/null +++ "b/xxl-job/doc/images/qq\347\276\244-\344\270\200\344\270\252xxl\345\220\214\345\255\246\350\277\233\344\272\20658.png" Binary files differ diff --git a/xxl-job/doc/images/xxl-logo.jpg b/xxl-job/doc/images/xxl-logo.jpg new file mode 100644 index 0000000..0169cce --- /dev/null +++ b/xxl-job/doc/images/xxl-logo.jpg Binary files differ diff --git a/xxl-job/doc/images/xxl-logo.png b/xxl-job/doc/images/xxl-logo.png new file mode 100644 index 0000000..045c006 --- /dev/null +++ b/xxl-job/doc/images/xxl-logo.png Binary files differ diff --git a/xxl-job/xxl-job-admin/Dockerfile b/xxl-job/xxl-job-admin/Dockerfile new file mode 100644 index 0000000..b5e78c2 --- /dev/null +++ b/xxl-job/xxl-job-admin/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:8-jre-slim +MAINTAINER xuxueli + +ENV PARAMS="" + +ENV TZ=PRC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +ADD target/xxl-job-admin-*.jar /application/app.jar + +ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /application/app.jar $PARAMS"] diff --git a/xxl-job/xxl-job-admin/pom.xml b/xxl-job/xxl-job-admin/pom.xml new file mode 100644 index 0000000..b10941f --- /dev/null +++ b/xxl-job/xxl-job-admin/pom.xml @@ -0,0 +1,113 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job</artifactId> + <version>2.3.1</version> + </parent> + <artifactId>xxl-job-admin</artifactId> + <packaging>jar</packaging> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>${spring-boot.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + + <!-- starter-web:spring-webmvc + autoconfigure + logback + yaml + tomcat --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <!-- starter-test:junit + spring-test + mockito --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- freemarker-starter --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-freemarker</artifactId> + </dependency> + + <!-- mail-starter --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mail</artifactId> + </dependency> + + <!-- starter-actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- mybatis-starter:mybatis + mybatis-spring + hikari(default) --> + <dependency> + <groupId>org.mybatis.spring.boot</groupId> + <artifactId>mybatis-spring-boot-starter</artifactId> + <version>${mybatis-spring-boot-starter.version}</version> + </dependency> + <!-- mysql --> + <dependency> + <groupId>mysql</groupId> + <artifactId>mysql-connector-java</artifactId> + <version>${mysql-connector-java.version}</version> + </dependency> + + <!-- xxl-job-core --> + <dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>${project.parent.version}</version> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <version>${spring-boot.version}</version> + <executions> + <execution> + <goals> + <goal>repackage</goal> + </goals> + </execution> + </executions> + </plugin> + <!-- docker --> + <plugin> + <groupId>com.spotify</groupId> + <artifactId>docker-maven-plugin</artifactId> + <version>0.4.13</version> + <configuration> + <!-- made of '[a-z0-9-_.]' --> + <imageName>${project.artifactId}:${project.version}</imageName> + <dockerDirectory>${project.basedir}</dockerDirectory> + <resources> + <resource> + <targetPath>/</targetPath> + <directory>${project.build.directory}</directory> + <include>${project.build.finalName}.jar</include> + </resource> + </resources> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java new file mode 100644 index 0000000..fce10a8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java @@ -0,0 +1,16 @@ +package com.xxl.job.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-10-28 00:38:13 + */ +@SpringBootApplication +public class XxlJobAdminApplication { + + public static void main(String[] args) { + SpringApplication.run(XxlJobAdminApplication.class, args); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java new file mode 100644 index 0000000..eb63f0b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +public class IndexController { + + @Resource + private XxlJobService xxlJobService; + @Resource + private LoginService loginService; + + + @RequestMapping("/") + public String index(Model model) { + + Map<String, Object> dashboardMap = xxlJobService.dashboardInfo(); + model.addAllAttributes(dashboardMap); + + return "index"; + } + + @RequestMapping("/chartInfo") + @ResponseBody + public ReturnT<Map<String, Object>> chartInfo(Date startDate, Date endDate) { + ReturnT<Map<String, Object>> chartInfo = xxlJobService.chartInfo(startDate, endDate); + return chartInfo; + } + + @RequestMapping("/toLogin") + @PermissionLimit(limit=false) + public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) { + if (loginService.ifLogin(request, response) != null) { + modelAndView.setView(new RedirectView("/",true,false)); + return modelAndView; + } + return new ModelAndView("login"); + } + + @RequestMapping(value="login", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT<String> loginDo(HttpServletRequest request, HttpServletResponse response, String userName, String password, String ifRemember){ + boolean ifRem = (ifRemember!=null && ifRemember.trim().length()>0 && "on".equals(ifRemember))?true:false; + return loginService.login(request, response, userName, password, ifRem); + } + + @RequestMapping(value="logout", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT<String> logout(HttpServletRequest request, HttpServletResponse response){ + return loginService.logout(request, response); + } + + @RequestMapping("/help") + public String help() { + + /*if (!PermissionInterceptor.ifLogin(request)) { + return "redirect:/toLogin"; + }*/ + + return "help"; + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java new file mode 100644 index 0000000..aa51e73 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java @@ -0,0 +1,72 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.GsonTool; +import com.xxl.job.core.util.XxlJobRemotingUtil; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * Created by xuxueli on 17/5/10. + */ +@Controller +@RequestMapping("/api") +public class JobApiController { + + @Resource + private AdminBiz adminBiz; + + /** + * api + * + * @param uri + * @param data + * @return + */ + @RequestMapping("/{uri}") + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT<String> api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) { + + // valid + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support."); + } + if (uri==null || uri.trim().length()==0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty."); + } + if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null + && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0 + && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong."); + } + + // services mapping + if ("callback".equals(uri)) { + List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class); + return adminBiz.callback(callbackParamList); + } else if ("registry".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registry(registryParam); + } else if ("registryRemove".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registryRemove(registryParam); + } else { + return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found."); + } + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java new file mode 100644 index 0000000..fe4a0e8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogGlueDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.glue.GlueTypeEnum; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.List; + +/** + * job code controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobcode") +public class JobCodeController { + + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, int jobId) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + List<XxlJobLogGlue> jobLogGlues = xxlJobLogGlueDao.findByJobId(jobId); + + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_gluetype_unvalid")); + } + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + + // Glue类型-字典 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); + + model.addAttribute("jobInfo", jobInfo); + model.addAttribute("jobLogGlues", jobLogGlues); + return "jobcode/jobcode.index"; + } + + @RequestMapping("/save") + @ResponseBody + public ReturnT<String> save(Model model, int id, String glueSource, String glueRemark) { + // valid + if (glueRemark==null) { + return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) ); + } + if (glueRemark.length()<4 || glueRemark.length()>100) { + return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_remark_limit")); + } + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(id); + if (exists_jobInfo == null) { + return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + + // update new code + exists_jobInfo.setGlueSource(glueSource); + exists_jobInfo.setGlueRemark(glueRemark); + exists_jobInfo.setGlueUpdatetime(new Date()); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + // log old code + XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue(); + xxlJobLogGlue.setJobId(exists_jobInfo.getId()); + xxlJobLogGlue.setGlueType(exists_jobInfo.getGlueType()); + xxlJobLogGlue.setGlueSource(glueSource); + xxlJobLogGlue.setGlueRemark(glueRemark); + + xxlJobLogGlue.setAddTime(new Date()); + xxlJobLogGlue.setUpdateTime(new Date()); + xxlJobLogGlueDao.save(xxlJobLogGlue); + + // remove code backup more than 30 + xxlJobLogGlueDao.removeOld(exists_jobInfo.getId(), 30); + + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java new file mode 100644 index 0000000..4bb4b90 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java @@ -0,0 +1,197 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobRegistryDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * job group controller + * @author xuxueli 2016-10-02 20:52:56 + */ +@Controller +@RequestMapping("/jobgroup") +public class JobGroupController { + + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + + @RequestMapping + public String index(Model model) { + return "jobgroup/jobgroup.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + public Map<String, Object> pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String appname, String title) { + + // page query + List<XxlJobGroup> list = xxlJobGroupDao.pageList(start, length, appname, title); + int list_count = xxlJobGroupDao.pageListCount(start, length, appname, title); + + // package result + Map<String, Object> maps = new HashMap<String, Object>(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/save") + @ResponseBody + public ReturnT<String> save(XxlJobGroup xxlJobGroup){ + + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT<String>(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) { + return new ReturnT<String>(500, "AppName"+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getAddressType()!=0) { + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") ); + } + + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.save(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/update") + @ResponseBody + public ReturnT<String> update(XxlJobGroup xxlJobGroup){ + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT<String>(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getAddressType() == 0) { + // 0=自动注册 + List<String> registryList = findRegistryByAppName(xxlJobGroup.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + addressListStr = ""; + for (String item:registryList) { + addressListStr += item + ","; + } + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + xxlJobGroup.setAddressList(addressListStr); + } else { + // 1=手动录入 + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.update(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + private List<String> findRegistryByAppName(String appnameParam){ + HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>(); + List<XxlJobRegistry> list = xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List<String> registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList<String>(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + return appAddressMap.get(appnameParam); + } + + @RequestMapping("/remove") + @ResponseBody + public ReturnT<String> remove(int id){ + + // valid + int count = xxlJobInfoDao.pageListCount(0, 10, id, -1, null, null, null); + if (count > 0) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_del_limit_0") ); + } + + List<XxlJobGroup> allList = xxlJobGroupDao.findAll(); + if (allList.size() == 1) { + return new ReturnT<String>(500, I18nUtil.getString("jobgroup_del_limit_1") ); + } + + int ret = xxlJobGroupDao.remove(id); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/loadById") + @ResponseBody + public ReturnT<XxlJobGroup> loadById(int id){ + XxlJobGroup jobGroup = xxlJobGroupDao.load(id); + return jobGroup!=null?new ReturnT<XxlJobGroup>(jobGroup):new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java new file mode 100644 index 0000000..ea314b3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java @@ -0,0 +1,180 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.text.ParseException; +import java.util.*; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobinfo") +public class JobInfoController { + private static Logger logger = LoggerFactory.getLogger(JobInfoController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobService xxlJobService; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) { + + // 枚举-字典 + model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values()); // 路由策略-列表 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); // Glue类型-字典 + model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values()); // 阻塞处理策略-字典 + model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values()); // 调度类型 + model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values()); // 调度过期策略 + + // 执行器列表 + List<XxlJobGroup> jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List<XxlJobGroup> jobGroupList = filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + model.addAttribute("jobGroup", jobGroup); + + return "jobinfo/jobinfo.index"; + } + + public static List<XxlJobGroup> filterJobGroupByRole(HttpServletRequest request, List<XxlJobGroup> jobGroupList_all){ + List<XxlJobGroup> jobGroupList = new ArrayList<>(); + if (jobGroupList_all!=null && jobGroupList_all.size()>0) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getRole() == 1) { + jobGroupList = jobGroupList_all; + } else { + List<String> groupIdStrs = new ArrayList<>(); + if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) { + groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(",")); + } + for (XxlJobGroup groupItem:jobGroupList_all) { + if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) { + jobGroupList.add(groupItem); + } + } + } + } + return jobGroupList; + } + public static void validPermission(HttpServletRequest request, int jobGroup) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (!loginUser.validPermission(jobGroup)) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]"); + } + } + + @RequestMapping("/pageList") + @ResponseBody + public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + } + + @RequestMapping("/add") + @ResponseBody + public ReturnT<String> add(XxlJobInfo jobInfo) { + return xxlJobService.add(jobInfo); + } + + @RequestMapping("/update") + @ResponseBody + public ReturnT<String> update(XxlJobInfo jobInfo) { + return xxlJobService.update(jobInfo); + } + + @RequestMapping("/remove") + @ResponseBody + public ReturnT<String> remove(int id) { + return xxlJobService.remove(id); + } + + @RequestMapping("/stop") + @ResponseBody + public ReturnT<String> pause(int id) { + return xxlJobService.stop(id); + } + + @RequestMapping("/start") + @ResponseBody + public ReturnT<String> start(int id) { + return xxlJobService.start(id); + } + + @RequestMapping("/trigger") + @ResponseBody + //@PermissionLimit(limit = false) + public ReturnT<String> triggerJob(int id, String executorParam, String addressList) { + // force cover job param + if (executorParam == null) { + executorParam = ""; + } + + JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); + return ReturnT.SUCCESS; + } + + @RequestMapping("/nextTriggerTime") + @ResponseBody + public ReturnT<List<String>> nextTriggerTime(String scheduleType, String scheduleConf) { + + XxlJobInfo paramXxlJobInfo = new XxlJobInfo(); + paramXxlJobInfo.setScheduleType(scheduleType); + paramXxlJobInfo.setScheduleConf(scheduleConf); + + List<String> result = new ArrayList<>(); + try { + Date lastTime = new Date(); + for (int i = 0; i < 5; i++) { + lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime); + if (lastTime != null) { + result.add(DateUtil.formatDateTime(lastTime)); + } else { + break; + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<List<String>>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage()); + } + return new ReturnT<List<String>>(result); + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java new file mode 100644 index 0000000..c64049d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java @@ -0,0 +1,233 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogDao; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.KillParam; +import com.xxl.job.core.biz.model.LogParam; +import com.xxl.job.core.biz.model.LogResult; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/joblog") +public class JobLogController { + private static Logger logger = LoggerFactory.getLogger(JobLogController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "0") Integer jobId) { + + // 执行器列表 + List<XxlJobGroup> jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List<XxlJobGroup> jobGroupList = JobInfoController.filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + + // 任务 + if (jobId > 0) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid")); + } + + model.addAttribute("jobInfo", jobInfo); + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + } + + return "joblog/joblog.index"; + } + + @RequestMapping("/getJobsByGroup") + @ResponseBody + public ReturnT<List<XxlJobInfo>> getJobsByGroup(int jobGroup){ + List<XxlJobInfo> list = xxlJobInfoDao.getJobsByGroup(jobGroup); + return new ReturnT<List<XxlJobInfo>>(list); + } + + @RequestMapping("/pageList") + @ResponseBody + public Map<String, Object> pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int jobId, int logStatus, String filterTime) { + + // valid permission + JobInfoController.validPermission(request, jobGroup); // 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup + + // parse param + Date triggerTimeStart = null; + Date triggerTimeEnd = null; + if (filterTime!=null && filterTime.trim().length()>0) { + String[] temp = filterTime.split(" - "); + if (temp.length == 2) { + triggerTimeStart = DateUtil.parseDateTime(temp[0]); + triggerTimeEnd = DateUtil.parseDateTime(temp[1]); + } + } + + // page query + List<XxlJobLog> list = xxlJobLogDao.pageList(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + int list_count = xxlJobLogDao.pageListCount(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + + // package result + Map<String, Object> maps = new HashMap<String, Object>(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/logDetailPage") + public String logDetailPage(int id, Model model){ + + // base check + ReturnT<String> logStatue = ReturnT.SUCCESS; + XxlJobLog jobLog = xxlJobLogDao.load(id); + if (jobLog == null) { + throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid")); + } + + model.addAttribute("triggerCode", jobLog.getTriggerCode()); + model.addAttribute("handleCode", jobLog.getHandleCode()); + model.addAttribute("executorAddress", jobLog.getExecutorAddress()); + model.addAttribute("triggerTime", jobLog.getTriggerTime().getTime()); + model.addAttribute("logId", jobLog.getId()); + return "joblog/joblog.detail"; + } + + @RequestMapping("/logDetailCat") + @ResponseBody + public ReturnT<LogResult> logDetailCat(String executorAddress, long triggerTime, long logId, int fromLineNum){ + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(executorAddress); + ReturnT<LogResult> logResult = executorBiz.log(new LogParam(triggerTime, logId, fromLineNum)); + + // is end + if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) { + XxlJobLog jobLog = xxlJobLogDao.load(logId); + if (jobLog.getHandleCode() > 0) { + logResult.getContent().setEnd(true); + } + } + + return logResult; + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<LogResult>(ReturnT.FAIL_CODE, e.getMessage()); + } + } + + @RequestMapping("/logKill") + @ResponseBody + public ReturnT<String> logKill(int id){ + // base check + XxlJobLog log = xxlJobLogDao.load(id); + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId()); + if (jobInfo==null) { + return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) { + return new ReturnT<String>(500, I18nUtil.getString("joblog_kill_log_limit")); + } + + // request of kill + ReturnT<String> runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress()); + runResult = executorBiz.kill(new KillParam(jobInfo.getId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + runResult = new ReturnT<String>(500, e.getMessage()); + } + + if (ReturnT.SUCCESS_CODE == runResult.getCode()) { + log.setHandleCode(ReturnT.FAIL_CODE); + log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():"")); + log.setHandleTime(new Date()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + return new ReturnT<String>(runResult.getMsg()); + } else { + return new ReturnT<String>(500, runResult.getMsg()); + } + } + + @RequestMapping("/clearLog") + @ResponseBody + public ReturnT<String> clearLog(int jobGroup, int jobId, int type){ + + Date clearBeforeTime = null; + int clearBeforeNum = 0; + if (type == 1) { + clearBeforeTime = DateUtil.addMonths(new Date(), -1); // 清理一个月之前日志数据 + } else if (type == 2) { + clearBeforeTime = DateUtil.addMonths(new Date(), -3); // 清理三个月之前日志数据 + } else if (type == 3) { + clearBeforeTime = DateUtil.addMonths(new Date(), -6); // 清理六个月之前日志数据 + } else if (type == 4) { + clearBeforeTime = DateUtil.addYears(new Date(), -1); // 清理一年之前日志数据 + } else if (type == 5) { + clearBeforeNum = 1000; // 清理一千条以前日志数据 + } else if (type == 6) { + clearBeforeNum = 10000; // 清理一万条以前日志数据 + } else if (type == 7) { + clearBeforeNum = 30000; // 清理三万条以前日志数据 + } else if (type == 8) { + clearBeforeNum = 100000; // 清理十万条以前日志数据 + } else if (type == 9) { + clearBeforeNum = 0; // 清理所有日志数据 + } else { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_clean_type_unvalid")); + } + + List<Long> logIds = null; + do { + logIds = xxlJobLogDao.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000); + if (logIds!=null && logIds.size()>0) { + xxlJobLogDao.clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java new file mode 100644 index 0000000..3f4c755 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java @@ -0,0 +1,179 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author xuxueli 2019-05-04 16:39:50 + */ +@Controller +@RequestMapping("/user") +public class UserController { + + @Resource + private XxlJobUserDao xxlJobUserDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + + // 执行器列表 + List<XxlJobGroup> groupList = xxlJobGroupDao.findAll(); + model.addAttribute("groupList", groupList); + + return "user/user.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String username, int role) { + + // page list + List<XxlJobUser> list = xxlJobUserDao.pageList(start, length, username, role); + int list_count = xxlJobUserDao.pageListCount(start, length, username, role); + + // filter + if (list!=null && list.size()>0) { + for (XxlJobUser item: list) { + item.setPassword(null); + } + } + + // package result + Map<String, Object> maps = new HashMap<String, Object>(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/add") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT<String> add(XxlJobUser xxlJobUser) { + + // valid username + if (!StringUtils.hasText(xxlJobUser.getUsername())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") ); + } + xxlJobUser.setUsername(xxlJobUser.getUsername().trim()); + if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // valid password + if (!StringUtils.hasText(xxlJobUser.getPassword())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") ); + } + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + + // check repeat + XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername()); + if (existUser != null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") ); + } + + // write + xxlJobUserDao.save(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT<String> update(HttpServletRequest request, XxlJobUser xxlJobUser) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getUsername().equals(xxlJobUser.getUsername())) { + return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + // valid password + if (StringUtils.hasText(xxlJobUser.getPassword())) { + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + } else { + xxlJobUser.setPassword(null); + } + + // write + xxlJobUserDao.update(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT<String> remove(HttpServletRequest request, int id) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getId() == id) { + return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + xxlJobUserDao.delete(id); + return ReturnT.SUCCESS; + } + + @RequestMapping("/updatePwd") + @ResponseBody + public ReturnT<String> updatePwd(HttpServletRequest request, String password){ + + // valid password + if (password==null || password.trim().length()==0){ + return new ReturnT<String>(ReturnT.FAIL.getCode(), "密码不可为空"); + } + password = password.trim(); + if (!(password.length()>=4 && password.length()<=20)) { + return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + + // md5 password + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + + // update pwd + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + + // do write + XxlJobUser existUser = xxlJobUserDao.loadByUserName(loginUser.getUsername()); + existUser.setPassword(md5Password); + xxlJobUserDao.update(existUser); + + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java new file mode 100644 index 0000000..379efd4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java @@ -0,0 +1,29 @@ +package com.xxl.job.admin.controller.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限限制 + * @author xuxueli 2015-12-12 18:29:02 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PermissionLimit { + + /** + * 登录拦截 (默认拦截) + */ + boolean limit() default true; + + /** + * 要求管理员权限 + * + * @return + */ + boolean adminuser() default false; + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java new file mode 100644 index 0000000..592d496 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java @@ -0,0 +1,42 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.core.util.FtlUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; + +/** + * push cookies to model as cookieMap + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class CookieInterceptor implements AsyncHandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + + // cookie + if (modelAndView!=null && request.getCookies()!=null && request.getCookies().length>0) { + HashMap<String, Cookie> cookieMap = new HashMap<String, Cookie>(); + for (Cookie ck : request.getCookies()) { + cookieMap.put(ck.getName(), ck); + } + modelAndView.addObject("cookieMap", cookieMap); + } + + // static method + if (modelAndView != null) { + modelAndView.addObject("I18nUtil", FtlUtil.generateStaticModel(I18nUtil.class.getName())); + } + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java new file mode 100644 index 0000000..840f0eb --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java @@ -0,0 +1,59 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.service.LoginService; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 权限拦截 + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class PermissionInterceptor implements AsyncHandlerInterceptor { + + @Resource + private LoginService loginService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (!(handler instanceof HandlerMethod)) { + return true; // proceed with the next interceptor + } + + // if need login + boolean needLogin = true; + boolean needAdminuser = false; + HandlerMethod method = (HandlerMethod)handler; + PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class); + if (permission!=null) { + needLogin = permission.limit(); + needAdminuser = permission.adminuser(); + } + + if (needLogin) { + XxlJobUser loginUser = loginService.ifLogin(request, response); + if (loginUser == null) { + response.setStatus(302); + response.setHeader("location", request.getContextPath()+"/toLogin"); + return false; + } + if (needAdminuser && loginUser.getRole()!=1) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit")); + } + request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser); + } + + return true; // proceed with the next interceptor + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java new file mode 100644 index 0000000..0be6ba6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java @@ -0,0 +1,28 @@ +package com.xxl.job.admin.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; + +/** + * web mvc config + * + * @author xuxueli 2018-04-02 20:48:20 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Resource + private PermissionInterceptor permissionInterceptor; + @Resource + private CookieInterceptor cookieInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(permissionInterceptor).addPathPatterns("/**"); + registry.addInterceptor(cookieInterceptor).addPathPatterns("/**"); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java new file mode 100644 index 0000000..114407b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java @@ -0,0 +1,66 @@ +package com.xxl.job.admin.controller.resolver; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.admin.core.util.JacksonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * common exception resolver + * + * @author xuxueli 2016-1-6 19:22:18 + */ +@Component +public class WebExceptionResolver implements HandlerExceptionResolver { + private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class); + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + + if (!(ex instanceof XxlJobException)) { + logger.error("WebExceptionResolver:{}", ex); + } + + // if json + boolean isJson = false; + if (handler instanceof HandlerMethod) { + HandlerMethod method = (HandlerMethod)handler; + ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class); + if (responseBody != null) { + isJson = true; + } + } + + // error result + ReturnT<String> errorResult = new ReturnT<String>(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "<br/>")); + + // response + ModelAndView mv = new ModelAndView(); + if (isJson) { + try { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JacksonUtil.writeValueAsString(errorResult)); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return mv; + } else { + + mv.addObject("exceptionMsg", errorResult.getMsg()); + mv.setViewName("/common/common.exception"); + return mv; + } + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java new file mode 100644 index 0000000..4165ff3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java @@ -0,0 +1,20 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; + +/** + * @author xuxueli 2020-01-19 + */ +public interface JobAlarm { + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java new file mode 100644 index 0000000..797dc90 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java @@ -0,0 +1,65 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class JobAlarmer implements ApplicationContextAware, InitializingBean { + private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class); + + private ApplicationContext applicationContext; + private List<JobAlarm> jobAlarmList; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + Map<String, JobAlarm> serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class); + if (serviceBeanMap != null && serviceBeanMap.size() > 0) { + jobAlarmList = new ArrayList<JobAlarm>(serviceBeanMap.values()); + } + } + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) { + + boolean result = false; + if (jobAlarmList!=null && jobAlarmList.size()>0) { + result = true; // success means all-success + for (JobAlarm alarm: jobAlarmList) { + boolean resultItem = false; + try { + resultItem = alarm.doAlarm(info, jobLog); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + if (!resultItem) { + result = false; + } + } + } + + return result; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java new file mode 100644 index 0000000..16e5218 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java @@ -0,0 +1,118 @@ +package com.xxl.job.admin.core.alarm.impl; + +import com.xxl.job.admin.core.alarm.JobAlarm; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.internet.MimeMessage; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * job alarm by email + * + * @author xuxueli 2020-01-19 + */ +@Component +public class EmailJobAlarm implements JobAlarm { + private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class); + + /** + * fail alarm + * + * @param jobLog + */ + @Override + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){ + boolean alarmResult = true; + + // send monitor email + if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) { + + // alarmContent + String alarmContent = "Alarm Job LogId=" + jobLog.getId(); + if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "<br>TriggerMsg=<br>" + jobLog.getTriggerMsg(); + } + if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "<br>HandleCode=" + jobLog.getHandleMsg(); + } + + // email info + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(Integer.valueOf(info.getJobGroup())); + String personal = I18nUtil.getString("admin_name_full"); + String title = I18nUtil.getString("jobconf_monitor"); + String content = MessageFormat.format(loadEmailJobAlarmTemplate(), + group!=null?group.getTitle():"null", + info.getId(), + info.getJobDesc(), + alarmContent); + + Set<String> emailSet = new HashSet<String>(Arrays.asList(info.getAlarmEmail().split(","))); + for (String email: emailSet) { + + // make mail + try { + MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal); + helper.setTo(email); + helper.setSubject(title); + helper.setText(content, true); + + XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e); + + alarmResult = false; + } + + } + } + + return alarmResult; + } + + /** + * load email job alarm template + * + * @return + */ + private static final String loadEmailJobAlarmTemplate(){ + String mailBodyTemplate = "<h5>" + I18nUtil.getString("jobconf_monitor_detail") + ":</span>" + + "<table border=\"1\" cellpadding=\"3\" style=\"border-collapse:collapse; width:80%;\" >\n" + + " <thead style=\"font-weight: bold;color: #ffffff;background-color: #ff8c00;\" >" + + " <tr>\n" + + " <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobgroup") +"</td>\n" + + " <td width=\"10%\" >"+ I18nUtil.getString("jobinfo_field_id") +"</td>\n" + + " <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobdesc") +"</td>\n" + + " <td width=\"10%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_title") +"</td>\n" + + " <td width=\"40%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_content") +"</td>\n" + + " </tr>\n" + + " </thead>\n" + + " <tbody>\n" + + " <tr>\n" + + " <td>{0}</td>\n" + + " <td>{1}</td>\n" + + " <td>{2}</td>\n" + + " <td>"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"</td>\n" + + " <td>{3}</td>\n" + + " </tr>\n" + + " </tbody>\n" + + "</table>"; + + return mailBodyTemplate; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java new file mode 100644 index 0000000..279ad7d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java @@ -0,0 +1,99 @@ +package com.xxl.job.admin.core.complete; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.MessageFormat; + +/** + * @author xuxueli 2020-10-30 20:43:10 + */ +public class XxlJobCompleter { + private static Logger logger = LoggerFactory.getLogger(XxlJobCompleter.class); + + /** + * common fresh handle entrance (limit only once) + * + * @param xxlJobLog + * @return + */ + public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) { + + // finish + finishJob(xxlJobLog); + + // text最大64kb 避免长度过长 + if (xxlJobLog.getHandleMsg().length() > 15000) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) ); + } + + // fresh handle + return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog); + } + + + /** + * do somethind to finish job + */ + private static void finishJob(XxlJobLog xxlJobLog){ + + // 1、handle success, to trigger child job + String triggerChildMsg = null; + if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) { + XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId()); + if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) { + triggerChildMsg = "<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<< </span><br>"; + + String[] childJobIds = xxlJobInfo.getChildJobId().split(","); + for (int i = 0; i < childJobIds.length; i++) { + int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1; + if (childJobId > 0) { + + JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null); + ReturnT<String> triggerChildResult = ReturnT.SUCCESS; + + // add msg + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"), + (i+1), + childJobIds.length, + childJobIds[i], + (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")), + triggerChildResult.getMsg()); + } else { + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"), + (i+1), + childJobIds.length, + childJobIds[i]); + } + } + + } + } + + if (triggerChildMsg != null) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg ); + } + + // 2、fix_delay trigger next + // on the way + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java new file mode 100644 index 0000000..380b8a5 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java @@ -0,0 +1,158 @@ +package com.xxl.job.admin.core.conf; + +import com.xxl.job.admin.core.alarm.JobAlarmer; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.dao.*; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.util.Arrays; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ + +@Component +public class XxlJobAdminConfig implements InitializingBean, DisposableBean { + + private static XxlJobAdminConfig adminConfig = null; + public static XxlJobAdminConfig getAdminConfig() { + return adminConfig; + } + + + // ---------------------- XxlJobScheduler ---------------------- + + private XxlJobScheduler xxlJobScheduler; + + @Override + public void afterPropertiesSet() throws Exception { + adminConfig = this; + + xxlJobScheduler = new XxlJobScheduler(); + xxlJobScheduler.init(); + } + + @Override + public void destroy() throws Exception { + xxlJobScheduler.destroy(); + } + + + // ---------------------- XxlJobScheduler ---------------------- + + // conf + @Value("${xxl.job.i18n}") + private String i18n; + + @Value("${xxl.job.accessToken}") + private String accessToken; + + @Value("${spring.mail.from}") + private String emailFrom; + + @Value("${xxl.job.triggerpool.fast.max}") + private int triggerPoolFastMax; + + @Value("${xxl.job.triggerpool.slow.max}") + private int triggerPoolSlowMax; + + @Value("${xxl.job.logretentiondays}") + private int logretentiondays; + + // dao, service + + @Resource + private XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + @Resource + private JavaMailSender mailSender; + @Resource + private DataSource dataSource; + @Resource + private JobAlarmer jobAlarmer; + + + public String getI18n() { + if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) { + return "zh_CN"; + } + return i18n; + } + + public String getAccessToken() { + return accessToken; + } + + public String getEmailFrom() { + return emailFrom; + } + + public int getTriggerPoolFastMax() { + if (triggerPoolFastMax < 200) { + return 200; + } + return triggerPoolFastMax; + } + + public int getTriggerPoolSlowMax() { + if (triggerPoolSlowMax < 100) { + return 100; + } + return triggerPoolSlowMax; + } + + public int getLogretentiondays() { + if (logretentiondays < 7) { + return -1; // Limit greater than or equal to 7, otherwise close + } + return logretentiondays; + } + + public XxlJobLogDao getXxlJobLogDao() { + return xxlJobLogDao; + } + + public XxlJobInfoDao getXxlJobInfoDao() { + return xxlJobInfoDao; + } + + public XxlJobRegistryDao getXxlJobRegistryDao() { + return xxlJobRegistryDao; + } + + public XxlJobGroupDao getXxlJobGroupDao() { + return xxlJobGroupDao; + } + + public XxlJobLogReportDao getXxlJobLogReportDao() { + return xxlJobLogReportDao; + } + + public JavaMailSender getMailSender() { + return mailSender; + } + + public DataSource getDataSource() { + return dataSource; + } + + public JobAlarmer getJobAlarmer() { + return jobAlarmer; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java new file mode 100644 index 0000000..fce2352 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java @@ -0,0 +1,1666 @@ +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package com.xxl.job.admin.core.cron; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; + +/** + * Provides a parser and evaluator for unix-like cron expressions. Cron + * expressions provide the ability to specify complex time combinations such as + * "At 8:00am every Monday through Friday" or "At 1:30am every + * last Friday of the month". + * <P> + * Cron expressions are comprised of 6 required fields and one optional field + * separated by white space. The fields respectively are described as follows: + * + * <table cellspacing="8"> + * <tr> + * <th align="left">Field Name</th> + * <th align="left"> </th> + * <th align="left">Allowed Values</th> + * <th align="left"> </th> + * <th align="left">Allowed Special Characters</th> + * </tr> + * <tr> + * <td align="left"><code>Seconds</code></td> + * <td align="left"> </th> + * <td align="left"><code>0-59</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * /</code></td> + * </tr> + * <tr> + * <td align="left"><code>Minutes</code></td> + * <td align="left"> </th> + * <td align="left"><code>0-59</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * /</code></td> + * </tr> + * <tr> + * <td align="left"><code>Hours</code></td> + * <td align="left"> </th> + * <td align="left"><code>0-23</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * /</code></td> + * </tr> + * <tr> + * <td align="left"><code>Day-of-month</code></td> + * <td align="left"> </th> + * <td align="left"><code>1-31</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * ? / L W</code></td> + * </tr> + * <tr> + * <td align="left"><code>Month</code></td> + * <td align="left"> </th> + * <td align="left"><code>0-11 or JAN-DEC</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * /</code></td> + * </tr> + * <tr> + * <td align="left"><code>Day-of-Week</code></td> + * <td align="left"> </th> + * <td align="left"><code>1-7 or SUN-SAT</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * ? / L #</code></td> + * </tr> + * <tr> + * <td align="left"><code>Year (Optional)</code></td> + * <td align="left"> </th> + * <td align="left"><code>empty, 1970-2199</code></td> + * <td align="left"> </th> + * <td align="left"><code>, - * /</code></td> + * </tr> + * </table> + * <P> + * The '*' character is used to specify all values. For example, "*" + * in the minute field means "every minute". + * <P> + * The '?' character is allowed for the day-of-month and day-of-week fields. It + * is used to specify 'no specific value'. This is useful when you need to + * specify something in one of the two fields, but not the other. + * <P> + * The '-' character is used to specify ranges For example "10-12" in + * the hour field means "the hours 10, 11 and 12". + * <P> + * The ',' character is used to specify additional values. For example + * "MON,WED,FRI" in the day-of-week field means "the days Monday, + * Wednesday, and Friday". + * <P> + * The '/' character is used to specify increments. For example "0/15" + * in the seconds field means "the seconds 0, 15, 30, and 45". And + * "5/15" in the seconds field means "the seconds 5, 20, 35, and + * 50". Specifying '*' before the '/' is equivalent to specifying 0 is + * the value to start with. Essentially, for each field in the expression, there + * is a set of numbers that can be turned on or off. For seconds and minutes, + * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to + * 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn + * on every "nth" value in the given set. Thus "7/6" in the + * month field only turns on month "7", it does NOT mean every 6th + * month, please note that subtlety. + * <P> + * The 'L' character is allowed for the day-of-month and day-of-week fields. + * This character is short-hand for "last", but it has different + * meaning in each of the two fields. For example, the value "L" in + * the day-of-month field means "the last day of the month" - day 31 + * for January, day 28 for February on non-leap years. If used in the + * day-of-week field by itself, it simply means "7" or + * "SAT". But if used in the day-of-week field after another value, it + * means "the last xxx day of the month" - for example "6L" + * means "the last friday of the month". You can also specify an offset + * from the last day of the month, such as "L-3" which would mean the third-to-last + * day of the calendar month. <i>When using the 'L' option, it is important not to + * specify lists, or ranges of values, as you'll get confusing/unexpected results.</i> + * <P> + * The 'W' character is allowed for the day-of-month field. This character + * is used to specify the weekday (Monday-Friday) nearest the given day. As an + * example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of + * the month". So if the 15th is a Saturday, the trigger will fire on + * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the + * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. + * However if you specify "1W" as the value for day-of-month, and the + * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not + * 'jump' over the boundary of a month's days. The 'W' character can only be + * specified when the day-of-month is a single day, not a range or list of days. + * <P> + * The 'L' and 'W' characters can also be combined for the day-of-month + * expression to yield 'LW', which translates to "last weekday of the + * month". + * <P> + * The '#' character is allowed for the day-of-week field. This character is + * used to specify "the nth" XXX day of the month. For example, the + * value of "6#3" in the day-of-week field means the third Friday of + * the month (day 6 = Friday and "#3" = the 3rd one in the month). + * Other examples: "2#1" = the first Monday of the month and + * "4#5" = the fifth Wednesday of the month. Note that if you specify + * "#5" and there is not 5 of the given day-of-week in the month, then + * no firing will occur that month. If the '#' character is used, there can + * only be one expression in the day-of-week field ("3#1,6#3" is + * not valid, since there are two expressions). + * <P> + * <!--The 'C' character is allowed for the day-of-month and day-of-week fields. + * This character is short-hand for "calendar". This means values are + * calculated against the associated calendar, if any. If no calendar is + * associated, then it is equivalent to having an all-inclusive calendar. A + * value of "5C" in the day-of-month field means "the first day included by the + * calendar on or after the 5th". A value of "1C" in the day-of-week field + * means "the first day included by the calendar on or after Sunday".--> + * <P> + * The legal characters and the names of months and days of the week are not + * case sensitive. + * + * <p> + * <b>NOTES:</b> + * <ul> + * <li>Support for specifying both a day-of-week and a day-of-month value is + * not complete (you'll need to use the '?' character in one of these fields). + * </li> + * <li>Overflowing ranges is supported - that is, having a larger number on + * the left hand side than the right. You might do 22-2 to catch 10 o'clock + * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is + * very important to note that overuse of overflowing ranges creates ranges + * that don't make sense and no effort has been made to determine which + * interpretation CronExpression chooses. An example would be + * "0 0 14-6 ? * FRI-MON". </li> + * </ul> + * </p> + * + * + * @author Sharada Jambula, James House + * @author Contributions from Mads Henderson + * @author Refactoring from CronTrigger to CronExpression by Aaron Craven + * + * Borrowed from quartz v2.3.1 + * + */ +public final class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20); + protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60); + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } + + private final String cronExpression; + private TimeZone timeZone = null; + protected transient TreeSet<Integer> seconds; + protected transient TreeSet<Integer> minutes; + protected transient TreeSet<Integer> hours; + protected transient TreeSet<Integer> daysOfMonth; + protected transient TreeSet<Integer> months; + protected transient TreeSet<Integer> daysOfWeek; + protected transient TreeSet<Integer> years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * Constructs a new <CODE>CronExpression</CODE> based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws java.text.ParseException + * if the string expression cannot be parsed into a valid + * <CODE>CronExpression</CODE> + */ + public CronExpression(String cronExpression) throws ParseException { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US); + + buildExpression(this.cronExpression); + } + + /** + * Constructs a new {@code CronExpression} as a copy of an existing + * instance. + * + * @param expression + * The existing cron expression to be copied + */ + public CronExpression(CronExpression expression) { + /* + * We don't call the other constructor here since we need to swallow the + * ParseException. We also elide some of the sanity checking as it is + * not logically trippable. + */ + this.cronExpression = expression.getCronExpression(); + try { + buildExpression(cronExpression); + } catch (ParseException ex) { + throw new AssertionError(); + } + if (expression.getTimeZone() != null) { + setTimeZone((TimeZone) expression.getTimeZone().clone()); + } + } + + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression + */ + public boolean isSatisfiedBy(Date date) { + Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + Date originalDate = testDateCal.getTime(); + + testDateCal.add(Calendar.SECOND, -1); + + Date timeAfter = getTimeAfter(testDateCal.getTime()); + + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } + + /** + * Returns the next date/time <I>after</I> the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time + */ + public Date getNextValidTimeAfter(Date date) { + return getTimeAfter(date); + } + + /** + * Returns the next date/time <I>after</I> the given date/time which does + * <I>not</I> satisfy the expression + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time + */ + public Date getNextInvalidTimeAfter(Date date) { + long difference = 1000; + + //move back to the nearest second so differences will be accurate + Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); + + Date newDate; + + //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. + + //keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if(newDate == null) + break; + + difference = newDate.getTime() - lastDate.getTime(); + + if (difference == 1000) { + lastDate = newDate; + } + } + + return new Date(lastDate.getTime() + 1000); + } + + /** + * Returns the time zone for which this <code>CronExpression</code> + * will be resolved. + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + + return timeZone; + } + + /** + * Sets the time zone for which this <code>CronExpression</code> + * will be resolved. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Returns the string representation of the <CODE>CronExpression</CODE> + * + * @return a string representation of the <CODE>CronExpression</CODE> + */ + @Override + public String toString() { + return cronExpression; + } + + /** + * Indicates whether the specified cron expression can be parsed into a + * valid cron expression + * + * @param cronExpression the expression to evaluate + * @return a boolean indicating whether the given expression is a valid cron + * expression + */ + public static boolean isValidExpression(String cronExpression) { + + try { + new CronExpression(cronExpression); + } catch (ParseException pe) { + return false; + } + + return true; + } + + public static void validateExpression(String cronExpression) throws ParseException { + + new CronExpression(cronExpression); + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Expression Parsing Functions + // + //////////////////////////////////////////////////////////////////////////// + + protected void buildExpression(String expression) throws ParseException { + expressionParsed = true; + + try { + + if (seconds == null) { + seconds = new TreeSet<Integer>(); + } + if (minutes == null) { + minutes = new TreeSet<Integer>(); + } + if (hours == null) { + hours = new TreeSet<Integer>(); + } + if (daysOfMonth == null) { + daysOfMonth = new TreeSet<Integer>(); + } + if (months == null) { + months = new TreeSet<Integer>(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet<Integer>(); + } + if (years == null) { + years = new TreeSet<Integer>(); + } + + int exprOn = SECOND; + + StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet<Integer> dow = getSet(DAY_OF_WEEK); + TreeSet<Integer> dom = getSet(DAY_OF_MONTH); + + // Copying the logic from the UnsupportedOperationException below + boolean dayOfMSpec = !dom.contains(NO_SPEC); + boolean dayOfWSpec = !dow.contains(NO_SPEC); + + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } catch (ParseException pe) { + throw pe; + } catch (Exception e) { + throw new ParseException("Illegal cron expression format (" + + e.toString() + ")", 0); + } + } + + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if(type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if(c == '-') { + ValueSet vs = getValue(0, s, i+1); + lastdayOffset = vs.value; + if(lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i+1); + i = vs.pos; + } + if(s.length() > i) { + c = s.charAt(i); + if(c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if(val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet<Integer> set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if(val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet<Integer> set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet<Integer> set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.Set<Integer> set) { + + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator<Integer> itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) { + + if (list.contains(NO_SPEC)) { + return "?"; + } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator<Integer> itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected int skipWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + } + + return i; + } + + protected int findNextWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + } + + return i; + } + + protected void addToSet(int val, int end, int incr, int type) + throws ParseException { + + TreeSet<Integer> set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); + } + } + + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } else { + set.add(NO_SPEC); + } + + return; + } + + int startAt = val; + int stopAt = end; + + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values + } + + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; + } + } + + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND : max = 60; break; + case MINUTE : max = 60; break; + case HOUR : max = 24; break; + case MONTH : max = 12; break; + case DAY_OF_WEEK : max = 7; break; + case DAY_OF_MONTH : max = 31; break; + case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); + default : throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; + } + + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { + i2 = max; + } + + set.add(i2); + } + } + } + + TreeSet<Integer> getSet(int type) { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + return null; + } + } + + protected ValueSet getValue(int v, String s, int i) { + char c = s.charAt(i); + StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; + } + c = s.charAt(i); + } + ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + protected int getNumericValue(String s, int i) { + int endOfVal = findNextWhiteSpace(i, s); + String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + protected int getMonthNumber(String s) { + Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + protected int getDayOfWeekNumber(String s) { + Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Computation Functions + // + //////////////////////////////////////////////////////////////////////////// + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + Calendar cl = new java.util.GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + + //if (endTime != null && cl.getTime().after(endTime)) return null; + if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet<Integer> st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if(!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if(t > day) { + mon++; + if(mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if(nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(java.util.Calendar.HOUR_OF_DAY, hour); + if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1); + } + } + + /** + * NOT YET IMPLEMENTED: Returns the time before the given time + * that the <code>CronExpression</code> matches. + */ + public Date getTimeBefore(Date endTime) { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + /** + * NOT YET IMPLEMENTED: Returns the final time that the + * <code>CronExpression</code> will match. + */ + public Date getFinalFireTime() { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + protected boolean isLeapYear(int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } + + protected int getLastDayOfMonth(int monthNum, int year) { + + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); + } + } + + + private void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java new file mode 100644 index 0000000..faa6063 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java @@ -0,0 +1,14 @@ +package com.xxl.job.admin.core.exception; + +/** + * @author xuxueli 2019-05-04 23:19:29 + */ +public class XxlJobException extends RuntimeException { + + public XxlJobException() { + } + public XxlJobException(String message) { + super(message); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java new file mode 100644 index 0000000..dde4b39 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java @@ -0,0 +1,77 @@ +package com.xxl.job.admin.core.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobGroup { + + private int id; + private String appname; + private String title; + private int addressType; // 执行器地址类型:0=自动注册、1=手动录入 + private String addressList; // 执行器地址列表,多地址逗号分隔(手动录入) + private Date updateTime; + + // registry list + private List<String> registryList; // 执行器地址列表(系统注册) + public List<String> getRegistryList() { + if (addressList!=null && addressList.trim().length()>0) { + registryList = new ArrayList<String>(Arrays.asList(addressList.split(","))); + } + return registryList; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getAppname() { + return appname; + } + + public void setAppname(String appname) { + this.appname = appname; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getAddressType() { + return addressType; + } + + public void setAddressType(int addressType) { + this.addressType = addressType; + } + + public String getAddressList() { + return addressList; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public void setAddressList(String addressList) { + this.addressList = addressList; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java new file mode 100644 index 0000000..e47b6dc --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java @@ -0,0 +1,237 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job info + * + * @author xuxueli 2016-1-12 18:25:49 + */ +public class XxlJobInfo { + + private int id; // 主键ID + + private int jobGroup; // 执行器主键ID + private String jobDesc; + + private Date addTime; + private Date updateTime; + + private String author; // 负责人 + private String alarmEmail; // 报警邮件 + + private String scheduleType; // 调度类型 + private String scheduleConf; // 调度配置,值含义取决于调度类型 + private String misfireStrategy; // 调度过期策略 + + private String executorRouteStrategy; // 执行器路由策略 + private String executorHandler; // 执行器,任务Handler名称 + private String executorParam; // 执行器,任务参数 + private String executorBlockStrategy; // 阻塞处理策略 + private int executorTimeout; // 任务执行超时时间,单位秒 + private int executorFailRetryCount; // 失败重试次数 + + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; // GLUE源代码 + private String glueRemark; // GLUE备注 + private Date glueUpdatetime; // GLUE更新时间 + + private String childJobId; // 子任务ID,多个逗号分隔 + + private int triggerStatus; // 调度状态:0-停止,1-运行 + private long triggerLastTime; // 上次调度时间 + private long triggerNextTime; // 下次调度时间 + + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public String getJobDesc() { + return jobDesc; + } + + public void setJobDesc(String jobDesc) { + this.jobDesc = jobDesc; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getAlarmEmail() { + return alarmEmail; + } + + public void setAlarmEmail(String alarmEmail) { + this.alarmEmail = alarmEmail; + } + + public String getScheduleType() { + return scheduleType; + } + + public void setScheduleType(String scheduleType) { + this.scheduleType = scheduleType; + } + + public String getScheduleConf() { + return scheduleConf; + } + + public void setScheduleConf(String scheduleConf) { + this.scheduleConf = scheduleConf; + } + + public String getMisfireStrategy() { + return misfireStrategy; + } + + public void setMisfireStrategy(String misfireStrategy) { + this.misfireStrategy = misfireStrategy; + } + + public String getExecutorRouteStrategy() { + return executorRouteStrategy; + } + + public void setExecutorRouteStrategy(String executorRouteStrategy) { + this.executorRouteStrategy = executorRouteStrategy; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorBlockStrategy() { + return executorBlockStrategy; + } + + public void setExecutorBlockStrategy(String executorBlockStrategy) { + this.executorBlockStrategy = executorBlockStrategy; + } + + public int getExecutorTimeout() { + return executorTimeout; + } + + public void setExecutorTimeout(int executorTimeout) { + this.executorTimeout = executorTimeout; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getGlueUpdatetime() { + return glueUpdatetime; + } + + public void setGlueUpdatetime(Date glueUpdatetime) { + this.glueUpdatetime = glueUpdatetime; + } + + public String getChildJobId() { + return childJobId; + } + + public void setChildJobId(String childJobId) { + this.childJobId = childJobId; + } + + public int getTriggerStatus() { + return triggerStatus; + } + + public void setTriggerStatus(int triggerStatus) { + this.triggerStatus = triggerStatus; + } + + public long getTriggerLastTime() { + return triggerLastTime; + } + + public void setTriggerLastTime(long triggerLastTime) { + this.triggerLastTime = triggerLastTime; + } + + public long getTriggerNextTime() { + return triggerNextTime; + } + + public void setTriggerNextTime(long triggerNextTime) { + this.triggerNextTime = triggerNextTime; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java new file mode 100644 index 0000000..7d3072a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java @@ -0,0 +1,157 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log, used to track trigger process + * @author xuxueli 2015-12-19 23:19:09 + */ +public class XxlJobLog { + + private long id; + + // job info + private int jobGroup; + private int jobId; + + // execute info + private String executorAddress; + private String executorHandler; + private String executorParam; + private String executorShardingParam; + private int executorFailRetryCount; + + // trigger info + private Date triggerTime; + private int triggerCode; + private String triggerMsg; + + // handle info + private Date handleTime; + private int handleCode; + private String handleMsg; + + // alarm info + private int alarmStatus; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getExecutorAddress() { + return executorAddress; + } + + public void setExecutorAddress(String executorAddress) { + this.executorAddress = executorAddress; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorShardingParam() { + return executorShardingParam; + } + + public void setExecutorShardingParam(String executorShardingParam) { + this.executorShardingParam = executorShardingParam; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public Date getTriggerTime() { + return triggerTime; + } + + public void setTriggerTime(Date triggerTime) { + this.triggerTime = triggerTime; + } + + public int getTriggerCode() { + return triggerCode; + } + + public void setTriggerCode(int triggerCode) { + this.triggerCode = triggerCode; + } + + public String getTriggerMsg() { + return triggerMsg; + } + + public void setTriggerMsg(String triggerMsg) { + this.triggerMsg = triggerMsg; + } + + public Date getHandleTime() { + return handleTime; + } + + public void setHandleTime(Date handleTime) { + this.handleTime = handleTime; + } + + public int getHandleCode() { + return handleCode; + } + + public void setHandleCode(int handleCode) { + this.handleCode = handleCode; + } + + public String getHandleMsg() { + return handleMsg; + } + + public void setHandleMsg(String handleMsg) { + this.handleMsg = handleMsg; + } + + public int getAlarmStatus() { + return alarmStatus; + } + + public void setAlarmStatus(int alarmStatus) { + this.alarmStatus = alarmStatus; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java new file mode 100644 index 0000000..2f59ffa --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java @@ -0,0 +1,75 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log for glue, used to track job code process + * @author xuxueli 2016-5-19 17:57:46 + */ +public class XxlJobLogGlue { + + private int id; + private int jobId; // 任务主键ID + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; + private String glueRemark; + private Date addTime; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java new file mode 100644 index 0000000..e58ff1a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java @@ -0,0 +1,54 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +public class XxlJobLogReport { + + private int id; + + private Date triggerDay; + + private int runningCount; + private int sucCount; + private int failCount; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Date getTriggerDay() { + return triggerDay; + } + + public void setTriggerDay(Date triggerDay) { + this.triggerDay = triggerDay; + } + + public int getRunningCount() { + return runningCount; + } + + public void setRunningCount(int runningCount) { + this.runningCount = runningCount; + } + + public int getSucCount() { + return sucCount; + } + + public void setSucCount(int sucCount) { + this.sucCount = sucCount; + } + + public int getFailCount() { + return failCount; + } + + public void setFailCount(int failCount) { + this.failCount = failCount; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java new file mode 100644 index 0000000..924d6d3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java @@ -0,0 +1,55 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobRegistry { + + private int id; + private String registryGroup; + private String registryKey; + private String registryValue; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getRegistryGroup() { + return registryGroup; + } + + public void setRegistryGroup(String registryGroup) { + this.registryGroup = registryGroup; + } + + public String getRegistryKey() { + return registryKey; + } + + public void setRegistryKey(String registryKey) { + this.registryKey = registryKey; + } + + public String getRegistryValue() { + return registryValue; + } + + public void setRegistryValue(String registryValue) { + this.registryValue = registryValue; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java new file mode 100644 index 0000000..db17327 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java @@ -0,0 +1,73 @@ +package com.xxl.job.admin.core.model; + +import org.springframework.util.StringUtils; + +/** + * @author xuxueli 2019-05-04 16:43:12 + */ +public class XxlJobUser { + + private int id; + private String username; // 账号 + private String password; // 密码 + private int role; // 角色:0-普通用户、1-管理员 + private String permission; // 权限:执行器ID列表,多个逗号分割 + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRole() { + return role; + } + + public void setRole(int role) { + this.role = role; + } + + public String getPermission() { + return permission; + } + + public void setPermission(String permission) { + this.permission = permission; + } + + // plugin + public boolean validPermission(int jobGroup){ + if (this.role == 1) { + return true; + } else { + if (StringUtils.hasText(this.permission)) { + for (String permissionItem : this.permission.split(",")) { + if (String.valueOf(jobGroup).equals(permissionItem)) { + return true; + } + } + } + return false; + } + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java new file mode 100644 index 0000000..b2dd151 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java @@ -0,0 +1,32 @@ +//package com.xxl.job.admin.core.jobbean; +// +//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +//import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +//import org.quartz.JobExecutionContext; +//import org.quartz.JobExecutionException; +//import org.quartz.JobKey; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.scheduling.quartz.QuartzJobBean; +// +///** +// * http job bean +// * “@DisallowConcurrentExecution” disable concurrent, thread size can not be only one, better given more +// * @author xuxueli 2015-12-17 18:20:34 +// */ +////@DisallowConcurrentExecution +//public class RemoteHttpJobBean extends QuartzJobBean { +// private static Logger logger = LoggerFactory.getLogger(RemoteHttpJobBean.class); +// +// @Override +// protected void executeInternal(JobExecutionContext context) +// throws JobExecutionException { +// +// // load jobId +// JobKey jobKey = context.getTrigger().getJobKey(); +// Integer jobId = Integer.valueOf(jobKey.getName()); +// +// +// } +// +//} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java new file mode 100644 index 0000000..1e62aa1 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java @@ -0,0 +1,413 @@ +//package com.xxl.job.admin.core.schedule; +// +//import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +//import com.xxl.job.admin.core.jobbean.RemoteHttpJobBean; +//import com.xxl.job.admin.core.model.XxlJobInfo; +//import com.xxl.job.admin.core.thread.JobFailMonitorHelper; +//import com.xxl.job.admin.core.thread.JobRegistryMonitorHelper; +//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +//import com.xxl.job.admin.core.util.I18nUtil; +//import com.xxl.job.core.biz.AdminBiz; +//import com.xxl.job.core.biz.ExecutorBiz; +//import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +//import com.xxl.rpc.remoting.invoker.XxlRpcInvokerFactory; +//import com.xxl.rpc.remoting.invoker.call.CallType; +//import com.xxl.rpc.remoting.invoker.reference.XxlRpcReferenceBean; +//import com.xxl.rpc.remoting.invoker.route.LoadBalance; +//import com.xxl.rpc.remoting.net.NetEnum; +//import com.xxl.rpc.remoting.net.impl.servlet.server.ServletServerHandler; +//import com.xxl.rpc.remoting.provider.XxlRpcProviderFactory; +//import com.xxl.rpc.serialize.Serializer; +//import org.quartz.*; +//import org.quartz.Trigger.TriggerState; +//import org.quartz.impl.triggers.CronTriggerImpl; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.util.Assert; +// +//import javax.servlet.ServletException; +//import javax.servlet.http.HttpServletRequest; +//import javax.servlet.http.HttpServletResponse; +//import java.io.IOException; +//import java.util.Date; +//import java.util.concurrent.ConcurrentHashMap; +// +///** +// * base quartz scheduler util +// * @author xuxueli 2015-12-19 16:13:53 +// */ +//public final class XxlJobDynamicScheduler { +// private static final Logger logger = LoggerFactory.getLogger(XxlJobDynamicScheduler_old.class); +// +// // ---------------------- param ---------------------- +// +// // scheduler +// private static Scheduler scheduler; +// public void setScheduler(Scheduler scheduler) { +// XxlJobDynamicScheduler_old.scheduler = scheduler; +// } +// +// +// // ---------------------- init + destroy ---------------------- +// public void start() throws Exception { +// // valid +// Assert.notNull(scheduler, "quartz scheduler is null"); +// +// // init i18n +// initI18n(); +// +// // admin registry monitor run +// JobRegistryMonitorHelper.getInstance().start(); +// +// // admin monitor run +// JobFailMonitorHelper.getInstance().start(); +// +// // admin-server +// initRpcProvider(); +// +// logger.info(">>>>>>>>> init xxl-job admin success."); +// } +// +// +// public void destroy() throws Exception { +// // admin trigger pool stop +// JobTriggerPoolHelper.toStop(); +// +// // admin registry stop +// JobRegistryMonitorHelper.getInstance().toStop(); +// +// // admin monitor stop +// JobFailMonitorHelper.getInstance().toStop(); +// +// // admin-server +// stopRpcProvider(); +// } +// +// +// // ---------------------- I18n ---------------------- +// +// private void initI18n(){ +// for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { +// item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); +// } +// } +// +// +// // ---------------------- admin rpc provider (no server version) ---------------------- +// private static ServletServerHandler servletServerHandler; +// private void initRpcProvider(){ +// // init +// XxlRpcProviderFactory xxlRpcProviderFactory = new XxlRpcProviderFactory(); +// xxlRpcProviderFactory.initConfig( +// NetEnum.NETTY_HTTP, +// Serializer.SerializeEnum.HESSIAN.getSerializer(), +// null, +// 0, +// XxlJobAdminConfig.getAdminConfig().getAccessToken(), +// null, +// null); +// +// // add services +// xxlRpcProviderFactory.addService(AdminBiz.class.getName(), null, XxlJobAdminConfig.getAdminConfig().getAdminBiz()); +// +// // servlet handler +// servletServerHandler = new ServletServerHandler(xxlRpcProviderFactory); +// } +// private void stopRpcProvider() throws Exception { +// XxlRpcInvokerFactory.getInstance().stop(); +// } +// public static void invokeAdminService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { +// servletServerHandler.handle(null, request, response); +// } +// +// +// // ---------------------- executor-client ---------------------- +// private static ConcurrentHashMap<String, ExecutorBiz> executorBizRepository = new ConcurrentHashMap<String, ExecutorBiz>(); +// public static ExecutorBiz getExecutorBiz(String address) throws Exception { +// // valid +// if (address==null || address.trim().length()==0) { +// return null; +// } +// +// // load-cache +// address = address.trim(); +// ExecutorBiz executorBiz = executorBizRepository.get(address); +// if (executorBiz != null) { +// return executorBiz; +// } +// +// // set-cache +// executorBiz = (ExecutorBiz) new XxlRpcReferenceBean( +// NetEnum.NETTY_HTTP, +// Serializer.SerializeEnum.HESSIAN.getSerializer(), +// CallType.SYNC, +// LoadBalance.ROUND, +// ExecutorBiz.class, +// null, +// 5000, +// address, +// XxlJobAdminConfig.getAdminConfig().getAccessToken(), +// null, +// null).getObject(); +// +// executorBizRepository.put(address, executorBiz); +// return executorBiz; +// } +// +// +// // ---------------------- schedule util ---------------------- +// +// /** +// * fill job info +// * +// * @param jobInfo +// */ +// public static void fillJobInfo(XxlJobInfo jobInfo) { +// +// String name = String.valueOf(jobInfo.getId()); +// +// // trigger key +// TriggerKey triggerKey = TriggerKey.triggerKey(name); +// try { +// +// // trigger cron +// Trigger trigger = scheduler.getTrigger(triggerKey); +// if (trigger!=null && trigger instanceof CronTriggerImpl) { +// String cronExpression = ((CronTriggerImpl) trigger).getCronExpression(); +// jobInfo.setJobCron(cronExpression); +// } +// +// // trigger state +// TriggerState triggerState = scheduler.getTriggerState(triggerKey); +// if (triggerState!=null) { +// jobInfo.setJobStatus(triggerState.name()); +// } +// +// //JobKey jobKey = new JobKey(jobInfo.getJobName(), String.valueOf(jobInfo.getJobGroup())); +// //JobDetail jobDetail = scheduler.getJobDetail(jobKey); +// //String jobClass = jobDetail.getJobClass().getName(); +// +// } catch (SchedulerException e) { +// logger.error(e.getMessage(), e); +// } +// } +// +// +// /** +// * add trigger + job +// * +// * @param jobName +// * @param cronExpression +// * @return +// * @throws SchedulerException +// */ +// public static boolean addJob(String jobName, String cronExpression) throws SchedulerException { +// // 1、job key +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// JobKey jobKey = new JobKey(jobName); +// +// // 2、valid +// if (scheduler.checkExists(triggerKey)) { +// return true; // PASS +// } +// +// // 3、corn trigger +// CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); // withMisfireHandlingInstructionDoNothing 忽略掉调度终止过程中忽略的调度 +// CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); +// +// // 4、job detail +// Class<? extends Job> jobClass_ = RemoteHttpJobBean.class; // Class.forName(jobInfo.getJobClass()); +// JobDetail jobDetail = JobBuilder.newJob(jobClass_).withIdentity(jobKey).build(); +// +// /*if (jobInfo.getJobData()!=null) { +// JobDataMap jobDataMap = jobDetail.getJobDataMap(); +// jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class)); +// // JobExecutionContext context.getMergedJobDataMap().get("mailGuid"); +// }*/ +// +// // 5、schedule job +// Date date = scheduler.scheduleJob(jobDetail, cronTrigger); +// +// logger.info(">>>>>>>>>>> addJob success(quartz), jobDetail:{}, cronTrigger:{}, date:{}", jobDetail, cronTrigger, date); +// return true; +// } +// +// +// /** +// * remove trigger + job +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// public static boolean removeJob(String jobName) throws SchedulerException { +// +// JobKey jobKey = new JobKey(jobName); +// scheduler.deleteJob(jobKey); +// +// /*TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// if (scheduler.checkExists(triggerKey)) { +// scheduler.unscheduleJob(triggerKey); // trigger + job +// }*/ +// +// logger.info(">>>>>>>>>>> removeJob success(quartz), jobKey:{}", jobKey); +// return true; +// } +// +// +// /** +// * updateJobCron +// * +// * @param jobName +// * @param cronExpression +// * @return +// * @throws SchedulerException +// */ +// public static boolean updateJobCron(String jobName, String cronExpression) throws SchedulerException { +// +// // 1、job key +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// // 2、valid +// if (!scheduler.checkExists(triggerKey)) { +// return true; // PASS +// } +// +// CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey); +// +// // 3、avoid repeat cron +// String oldCron = oldTrigger.getCronExpression(); +// if (oldCron.equals(cronExpression)){ +// return true; // PASS +// } +// +// // 4、new cron trigger +// CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); +// oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); +// +// // 5、rescheduleJob +// scheduler.rescheduleJob(triggerKey, oldTrigger); +// +// /* +// JobKey jobKey = new JobKey(jobName); +// +// // old job detail +// JobDetail jobDetail = scheduler.getJobDetail(jobKey); +// +// // new trigger +// HashSet<Trigger> triggerSet = new HashSet<Trigger>(); +// triggerSet.add(cronTrigger); +// // cover trigger of job detail +// scheduler.scheduleJob(jobDetail, triggerSet, true);*/ +// +// logger.info(">>>>>>>>>>> resumeJob success, JobName:{}", jobName); +// return true; +// } +// +// +// /** +// * pause +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// /*public static boolean pauseJob(String jobName) throws SchedulerException { +// +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// boolean result = false; +// if (scheduler.checkExists(triggerKey)) { +// scheduler.pauseTrigger(triggerKey); +// result = true; +// } +// +// logger.info(">>>>>>>>>>> pauseJob {}, triggerKey:{}", (result?"success":"fail"),triggerKey); +// return result; +// }*/ +// +// +// /** +// * resume +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// /*public static boolean resumeJob(String jobName) throws SchedulerException { +// +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// boolean result = false; +// if (scheduler.checkExists(triggerKey)) { +// scheduler.resumeTrigger(triggerKey); +// result = true; +// } +// +// logger.info(">>>>>>>>>>> resumeJob {}, triggerKey:{}", (result?"success":"fail"), triggerKey); +// return result; +// }*/ +// +// +// /** +// * run +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// /*public static boolean triggerJob(String jobName) throws SchedulerException { +// // TriggerKey : name + group +// JobKey jobKey = new JobKey(jobName); +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// boolean result = false; +// if (scheduler.checkExists(triggerKey)) { +// scheduler.triggerJob(jobKey); +// result = true; +// logger.info(">>>>>>>>>>> runJob success, jobKey:{}", jobKey); +// } else { +// logger.info(">>>>>>>>>>> runJob fail, jobKey:{}", jobKey); +// } +// return result; +// }*/ +// +// +// /** +// * finaAllJobList +// * +// * @return +// *//* +// @Deprecated +// public static List<Map<String, Object>> finaAllJobList(){ +// List<Map<String, Object>> jobList = new ArrayList<Map<String,Object>>(); +// +// try { +// if (scheduler.getJobGroupNames()==null || scheduler.getJobGroupNames().size()==0) { +// return null; +// } +// String groupName = scheduler.getJobGroupNames().get(0); +// Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)); +// if (jobKeys!=null && jobKeys.size()>0) { +// for (JobKey jobKey : jobKeys) { +// TriggerKey triggerKey = TriggerKey.triggerKey(jobKey.getName(), Scheduler.DEFAULT_GROUP); +// Trigger trigger = scheduler.getTrigger(triggerKey); +// JobDetail jobDetail = scheduler.getJobDetail(jobKey); +// TriggerState triggerState = scheduler.getTriggerState(triggerKey); +// Map<String, Object> jobMap = new HashMap<String, Object>(); +// jobMap.put("TriggerKey", triggerKey); +// jobMap.put("Trigger", trigger); +// jobMap.put("JobDetail", jobDetail); +// jobMap.put("TriggerState", triggerState); +// jobList.add(jobMap); +// } +// } +// +// } catch (SchedulerException e) { +// logger.error(e.getMessage(), e); +// return null; +// } +// return jobList; +// }*/ +// +//} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java new file mode 100644 index 0000000..ad07430 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java @@ -0,0 +1,58 @@ +//package com.xxl.job.admin.core.quartz; +// +//import org.quartz.SchedulerConfigException; +//import org.quartz.spi.ThreadPool; +// +///** +// * single thread pool, for async trigger +// * +// * @author xuxueli 2019-03-06 +// */ +//public class XxlJobThreadPool implements ThreadPool { +// +// @Override +// public boolean runInThread(Runnable runnable) { +// +// // async run +// runnable.run(); +// return true; +// +// //return false; +// } +// +// @Override +// public int blockForAvailableThreads() { +// return 1; +// } +// +// @Override +// public void initialize() throws SchedulerConfigException { +// +// } +// +// @Override +// public void shutdown(boolean waitForJobsToComplete) { +// +// } +// +// @Override +// public int getPoolSize() { +// return 1; +// } +// +// @Override +// public void setInstanceId(String schedInstId) { +// +// } +// +// @Override +// public void setInstanceName(String schedName) { +// +// } +// +// // support +// public void setThreadCount(int count) { +// // +// } +// +//} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java new file mode 100644 index 0000000..7fff93a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.admin.core.route.strategy.*; +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * Created by xuxueli on 17/3/10. + */ +public enum ExecutorRouteStrategyEnum { + + FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()), + LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()), + ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()), + RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()), + CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()), + LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()), + LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()), + FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()), + BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()), + SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null); + + ExecutorRouteStrategyEnum(String title, ExecutorRouter router) { + this.title = title; + this.router = router; + } + + private String title; + private ExecutorRouter router; + + public String getTitle() { + return title; + } + public ExecutorRouter getRouter() { + return router; + } + + public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){ + if (name != null) { + for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + } + return defaultItem; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java new file mode 100644 index 0000000..5de9a1d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public abstract class ExecutorRouter { + protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class); + + /** + * route address + * + * @param addressList + * @return ReturnT.content=address + */ + public abstract ReturnT<String> route(TriggerParam triggerParam, List<String> addressList); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java new file mode 100644 index 0000000..868560f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.IdleBeatParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteBusyover extends ExecutorRouter { + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + StringBuffer idleBeatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT<String> idleBeatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + idleBeatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e ); + } + idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"<br><br>":"") + .append(I18nUtil.getString("jobconf_idleBeat") + ":") + .append("<br>address:").append(address) + .append("<br>code:").append(idleBeatResult.getCode()) + .append("<br>msg:").append(idleBeatResult.getMsg()); + + // beat success + if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) { + idleBeatResult.setMsg(idleBeatResultSB.toString()); + idleBeatResult.setContent(address); + return idleBeatResult; + } + } + + return new ReturnT<String>(ReturnT.FAIL_CODE, idleBeatResultSB.toString()); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java new file mode 100644 index 0000000..41ac671 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java @@ -0,0 +1,85 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器; + * a、virtual node:解决不均衡问题 + * b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围 + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteConsistentHash extends ExecutorRouter { + + private static int VIRTUAL_NODE_NUM = 100; + + /** + * get hash code on 2^32 ring (md5散列的方式计算hash值) + * @param key + * @return + */ + private static long hash(String key) { + + // md5 byte + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + md5.reset(); + byte[] keyBytes = null; + try { + keyBytes = key.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unknown string :" + key, e); + } + + md5.update(keyBytes); + byte[] digest = md5.digest(); + + // hash code, Truncate to 32-bits + long hashCode = ((long) (digest[3] & 0xFF) << 24) + | ((long) (digest[2] & 0xFF) << 16) + | ((long) (digest[1] & 0xFF) << 8) + | (digest[0] & 0xFF); + + long truncateHashCode = hashCode & 0xffffffffL; + return truncateHashCode; + } + + public String hashJob(int jobId, List<String> addressList) { + + // ------A1------A2-------A3------ + // -----------J1------------------ + TreeMap<Long, String> addressRing = new TreeMap<Long, String>(); + for (String address: addressList) { + for (int i = 0; i < VIRTUAL_NODE_NUM; i++) { + long addressHash = hash("SHARD-" + address + "-NODE-" + i); + addressRing.put(addressHash, address); + } + } + + long jobHash = hash(String.valueOf(jobId)); + SortedMap<Long, String> lastRing = addressRing.tailMap(jobHash); + if (!lastRing.isEmpty()) { + return lastRing.get(lastRing.firstKey()); + } + return addressRing.firstEntry().getValue(); + } + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + String address = hashJob(triggerParam.getJobId(), addressList); + return new ReturnT<String>(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java new file mode 100644 index 0000000..a2e4c90 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFailover extends ExecutorRouter { + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + + StringBuffer beatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT<String> beatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + beatResult = executorBiz.beat(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + beatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e ); + } + beatResultSB.append( (beatResultSB.length()>0)?"<br><br>":"") + .append(I18nUtil.getString("jobconf_beat") + ":") + .append("<br>address:").append(address) + .append("<br>code:").append(beatResult.getCode()) + .append("<br>msg:").append(beatResult.getMsg()); + + // beat success + if (beatResult.getCode() == ReturnT.SUCCESS_CODE) { + + beatResult.setMsg(beatResultSB.toString()); + beatResult.setContent(address); + return beatResult; + } + } + return new ReturnT<String>(ReturnT.FAIL_CODE, beatResultSB.toString()); + + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java new file mode 100644 index 0000000..de4d7af --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFirst extends ExecutorRouter { + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList){ + return new ReturnT<String>(addressList.get(0)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java new file mode 100644 index 0000000..9df1972 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,使用频率最低的优先被选举 + * a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLFU extends ExecutorRouter { + + private static ConcurrentMap<Integer, HashMap<String, Integer>> jobLfuMap = new ConcurrentHashMap<Integer, HashMap<String, Integer>>(); + private static long CACHE_VALID_TIME = 0; + + public String route(int jobId, List<String> addressList) { + + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + jobLfuMap.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + // lfu item init + HashMap<String, Integer> lfuItemMap = jobLfuMap.get(jobId); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList; + if (lfuItemMap == null) { + lfuItemMap = new HashMap<String, Integer>(); + jobLfuMap.putIfAbsent(jobId, lfuItemMap); // 避免重复覆盖 + } + + // put new + for (String address: addressList) { + if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) { + lfuItemMap.put(address, new Random().nextInt(addressList.size())); // 初始化时主动Random一次,缓解首次压力 + } + } + // remove old + List<String> delKeys = new ArrayList<>(); + for (String existKey: lfuItemMap.keySet()) { + if (!addressList.contains(existKey)) { + delKeys.add(existKey); + } + } + if (delKeys.size() > 0) { + for (String delKey: delKeys) { + lfuItemMap.remove(delKey); + } + } + + // load least userd count address + List<Map.Entry<String, Integer>> lfuItemList = new ArrayList<Map.Entry<String, Integer>>(lfuItemMap.entrySet()); + Collections.sort(lfuItemList, new Comparator<Map.Entry<String, Integer>>() { + @Override + public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { + return o1.getValue().compareTo(o2.getValue()); + } + }); + + Map.Entry<String, Integer> addressItem = lfuItemList.get(0); + String minAddress = addressItem.getKey(); + addressItem.setValue(addressItem.getValue() + 1); + + return addressItem.getKey(); + } + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT<String>(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java new file mode 100644 index 0000000..2d54006 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java @@ -0,0 +1,76 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,最久为使用的优先被选举 + * a、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b(*)、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLRU extends ExecutorRouter { + + private static ConcurrentMap<Integer, LinkedHashMap<String, String>> jobLRUMap = new ConcurrentHashMap<Integer, LinkedHashMap<String, String>>(); + private static long CACHE_VALID_TIME = 0; + + public String route(int jobId, List<String> addressList) { + + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + jobLRUMap.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + // init lru + LinkedHashMap<String, String> lruItem = jobLRUMap.get(jobId); + if (lruItem == null) { + /** + * LinkedHashMap + * a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期; + * b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法; + */ + lruItem = new LinkedHashMap<String, String>(16, 0.75f, true); + jobLRUMap.putIfAbsent(jobId, lruItem); + } + + // put new + for (String address: addressList) { + if (!lruItem.containsKey(address)) { + lruItem.put(address, address); + } + } + // remove old + List<String> delKeys = new ArrayList<>(); + for (String existKey: lruItem.keySet()) { + if (!addressList.contains(existKey)) { + delKeys.add(existKey); + } + } + if (delKeys.size() > 0) { + for (String delKey: delKeys) { + lruItem.remove(delKey); + } + } + + // load + String eldestKey = lruItem.entrySet().iterator().next().getKey(); + String eldestValue = lruItem.get(eldestKey); + return eldestValue; + } + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT<String>(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java new file mode 100644 index 0000000..4ff3cf6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLast extends ExecutorRouter { + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + return new ReturnT<String>(addressList.get(addressList.size()-1)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java new file mode 100644 index 0000000..5ea4a38 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java @@ -0,0 +1,23 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRandom extends ExecutorRouter { + + private static Random localRandom = new Random(); + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + String address = addressList.get(localRandom.nextInt(addressList.size())); + return new ReturnT<String>(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java new file mode 100644 index 0000000..d0ea2ba --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRound extends ExecutorRouter { + + private static ConcurrentMap<Integer, AtomicInteger> routeCountEachJob = new ConcurrentHashMap<>(); + private static long CACHE_VALID_TIME = 0; + + private static int count(int jobId) { + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + routeCountEachJob.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + AtomicInteger count = routeCountEachJob.get(jobId); + if (count == null || count.get() > 1000000) { + // 初始化时主动Random一次,缓解首次压力 + count = new AtomicInteger(new Random().nextInt(100)); + } else { + // count++ + count.addAndGet(1); + } + routeCountEachJob.put(jobId, count); + return count.get(); + } + + @Override + public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) { + String address = addressList.get(count(triggerParam.getJobId())%addressList.size()); + return new ReturnT<String>(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java new file mode 100644 index 0000000..0b9b4a9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java @@ -0,0 +1,39 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum MisfireStrategyEnum { + + /** + * do nothing + */ + DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing")), + + /** + * fire once now + */ + FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now")); + + private String title; + + MisfireStrategyEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){ + for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java new file mode 100644 index 0000000..aa334fd --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum ScheduleTypeEnum { + + NONE(I18nUtil.getString("schedule_type_none")), + + /** + * schedule by cron + */ + CRON(I18nUtil.getString("schedule_type_cron")), + + /** + * schedule by fixed rate (in seconds) + */ + FIX_RATE(I18nUtil.getString("schedule_type_fix_rate")), + + /** + * schedule by fix delay (in seconds), after the last time + */ + /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/; + + private String title; + + ScheduleTypeEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){ + for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java new file mode 100644 index 0000000..bb2cda8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java @@ -0,0 +1,101 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.thread.*; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.client.ExecutorBizClient; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author xuxueli 2018-10-28 00:18:17 + */ + +public class XxlJobScheduler { + private static final Logger logger = LoggerFactory.getLogger(XxlJobScheduler.class); + + + public void init() throws Exception { + // init i18n + initI18n(); + + // admin trigger pool start + JobTriggerPoolHelper.toStart(); + + // admin registry monitor run + JobRegistryHelper.getInstance().start(); + + // admin fail-monitor run + JobFailMonitorHelper.getInstance().start(); + + // admin lose-monitor run ( depend on JobTriggerPoolHelper ) + JobCompleteHelper.getInstance().start(); + + // admin log report start + JobLogReportHelper.getInstance().start(); + + // start-schedule ( depend on JobTriggerPoolHelper ) + JobScheduleHelper.getInstance().start(); + + logger.info(">>>>>>>>> init xxl-job admin success."); + } + + + public void destroy() throws Exception { + + // stop-schedule + JobScheduleHelper.getInstance().toStop(); + + // admin log report stop + JobLogReportHelper.getInstance().toStop(); + + // admin lose-monitor stop + JobCompleteHelper.getInstance().toStop(); + + // admin fail-monitor stop + JobFailMonitorHelper.getInstance().toStop(); + + // admin registry stop + JobRegistryHelper.getInstance().toStop(); + + // admin trigger pool stop + JobTriggerPoolHelper.toStop(); + + } + + // ---------------------- I18n ---------------------- + + private void initI18n(){ + for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { + item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); + } + } + + // ---------------------- executor-client ---------------------- + private static ConcurrentMap<String, ExecutorBiz> executorBizRepository = new ConcurrentHashMap<String, ExecutorBiz>(); + public static ExecutorBiz getExecutorBiz(String address) throws Exception { + // valid + if (address==null || address.trim().length()==0) { + return null; + } + + // load-cache + address = address.trim(); + ExecutorBiz executorBiz = executorBizRepository.get(address); + if (executorBiz != null) { + return executorBiz; + } + + // set-cache + executorBiz = new ExecutorBizClient(address, XxlJobAdminConfig.getAdminConfig().getAccessToken()); + + executorBizRepository.put(address, executorBiz); + return executorBiz; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java new file mode 100644 index 0000000..5698926 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java @@ -0,0 +1,184 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; + +/** + * job lose-monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobCompleteHelper { + private static Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class); + + private static JobCompleteHelper instance = new JobCompleteHelper(); + public static JobCompleteHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private ThreadPoolExecutor callbackThreadPool = null; + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + + // for callback + callbackThreadPool = new ThreadPoolExecutor( + 2, + 20, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(3000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now)."); + } + }); + + + // for monitor + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // wait for JobTriggerPoolHelper-init + try { + TimeUnit.MILLISECONDS.sleep(50); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + // monitor + while (!toStop) { + try { + // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; + Date losedTime = DateUtil.addMinutes(new Date(), -10); + List<Long> losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime); + + if (losedJobIds!=null && losedJobIds.size()>0) { + for (Long logId: losedJobIds) { + + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setId(logId); + + jobLog.setHandleTime(new Date()); + jobLog.setHandleCode(ReturnT.FAIL_CODE); + jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") ); + + XxlJobCompleter.updateHandleInfoAndFinish(jobLog); + } + + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(60); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobLosedMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + callbackThreadPool.shutdownNow(); + + // stop monitorThread (interrupt and wait) + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) { + + callbackThreadPool.execute(new Runnable() { + @Override + public void run() { + for (HandleCallbackParam handleCallbackParam: callbackParamList) { + ReturnT<String> callbackResult = callback(handleCallbackParam); + logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}", + (callbackResult.getCode()== ReturnT.SUCCESS_CODE?"success":"fail"), handleCallbackParam, callbackResult); + } + } + }); + + return ReturnT.SUCCESS; + } + + private ReturnT<String> callback(HandleCallbackParam handleCallbackParam) { + // valid log item + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(handleCallbackParam.getLogId()); + if (log == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "log item not found."); + } + if (log.getHandleCode() > 0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "log repeate callback."); // avoid repeat callback, trigger child job etc + } + + // handle msg + StringBuffer handleMsg = new StringBuffer(); + if (log.getHandleMsg()!=null) { + handleMsg.append(log.getHandleMsg()).append("<br>"); + } + if (handleCallbackParam.getHandleMsg() != null) { + handleMsg.append(handleCallbackParam.getHandleMsg()); + } + + // success, save log + log.setHandleTime(new Date()); + log.setHandleCode(handleCallbackParam.getHandleCode()); + log.setHandleMsg(handleMsg.toString()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + + return ReturnT.SUCCESS; + } + + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java new file mode 100644 index 0000000..8409d7b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java @@ -0,0 +1,110 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * job monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobFailMonitorHelper { + private static Logger logger = LoggerFactory.getLogger(JobFailMonitorHelper.class); + + private static JobFailMonitorHelper instance = new JobFailMonitorHelper(); + public static JobFailMonitorHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // monitor + while (!toStop) { + try { + + List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000); + if (failLogIds!=null && !failLogIds.isEmpty()) { + for (long failLogId: failLogIds) { + + // lock log + int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1); + if (lockRet < 1) { + continue; + } + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId); + XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId()); + + // 1、fail retry monitor + if (log.getExecutorFailRetryCount() > 0) { + JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); + String retryMsg = "<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<< </span><br>"; + log.setTriggerMsg(log.getTriggerMsg() + retryMsg); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log); + } + + // 2、fail alarm monitor + int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 + if (info != null) { + boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log); + newAlarmStatus = alarmResult?2:3; + } else { + newAlarmStatus = 1; + } + + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(10); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobFailMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java new file mode 100644 index 0000000..2387a0c --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java @@ -0,0 +1,152 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * job log report helper + * + * @author xuxueli 2019-11-22 + */ +public class JobLogReportHelper { + private static Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class); + + private static JobLogReportHelper instance = new JobLogReportHelper(); + public static JobLogReportHelper getInstance(){ + return instance; + } + + + private Thread logrThread; + private volatile boolean toStop = false; + public void start(){ + logrThread = new Thread(new Runnable() { + + @Override + public void run() { + + // last clean log time + long lastCleanLogTime = 0; + + + while (!toStop) { + + // 1、log-report refresh: refresh log report in 3 days + try { + + for (int i = 0; i < 3; i++) { + + // today + Calendar itemDay = Calendar.getInstance(); + itemDay.add(Calendar.DAY_OF_MONTH, -i); + itemDay.set(Calendar.HOUR_OF_DAY, 0); + itemDay.set(Calendar.MINUTE, 0); + itemDay.set(Calendar.SECOND, 0); + itemDay.set(Calendar.MILLISECOND, 0); + + Date todayFrom = itemDay.getTime(); + + itemDay.set(Calendar.HOUR_OF_DAY, 23); + itemDay.set(Calendar.MINUTE, 59); + itemDay.set(Calendar.SECOND, 59); + itemDay.set(Calendar.MILLISECOND, 999); + + Date todayTo = itemDay.getTime(); + + // refresh log-report every minute + XxlJobLogReport xxlJobLogReport = new XxlJobLogReport(); + xxlJobLogReport.setTriggerDay(todayFrom); + xxlJobLogReport.setRunningCount(0); + xxlJobLogReport.setSucCount(0); + xxlJobLogReport.setFailCount(0); + + Map<String, Object> triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo); + if (triggerCountMap!=null && triggerCountMap.size()>0) { + int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0; + int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0; + int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0; + int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc; + + xxlJobLogReport.setRunningCount(triggerDayCountRunning); + xxlJobLogReport.setSucCount(triggerDayCountSuc); + xxlJobLogReport.setFailCount(triggerDayCountFail); + } + + // do refresh + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e); + } + } + + // 2、log-clean: switch open & once each day + if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0 + && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) { + + // expire-time + Calendar expiredDay = Calendar.getInstance(); + expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays()); + expiredDay.set(Calendar.HOUR_OF_DAY, 0); + expiredDay.set(Calendar.MINUTE, 0); + expiredDay.set(Calendar.SECOND, 0); + expiredDay.set(Calendar.MILLISECOND, 0); + Date clearBeforeTime = expiredDay.getTime(); + + // clean expired log + List<Long> logIds = null; + do { + logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000); + if (logIds!=null && logIds.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + // update clean time + lastCleanLogTime = System.currentTimeMillis(); + } + + try { + TimeUnit.MINUTES.sleep(1); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job log report thread stop"); + + } + }); + logrThread.setDaemon(true); + logrThread.setName("xxl-job, admin JobLogReportHelper"); + logrThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + logrThread.interrupt(); + try { + logrThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java new file mode 100644 index 0000000..37edfd9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.concurrent.*; + +/** + * job registry instance + * @author xuxueli 2016-10-02 19:10:24 + */ +public class JobRegistryHelper { + private static Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class); + + private static JobRegistryHelper instance = new JobRegistryHelper(); + public static JobRegistryHelper getInstance(){ + return instance; + } + + private ThreadPoolExecutor registryOrRemoveThreadPool = null; + private Thread registryMonitorThread; + private volatile boolean toStop = false; + + public void start(){ + + // for registry or remove + registryOrRemoveThreadPool = new ThreadPoolExecutor( + 2, + 10, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now)."); + } + }); + + // for monitor + registryMonitorThread = new Thread(new Runnable() { + @Override + public void run() { + while (!toStop) { + try { + // auto registry group + List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0); + if (groupList!=null && !groupList.isEmpty()) { + + // remove dead address (admin/executor) + List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (ids!=null && ids.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids); + } + + // fresh online address (admin/executor) + HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>(); + List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List<String> registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList<String>(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + + // fresh group address + for (XxlJobGroup group: groupList) { + List<String> registryList = appAddressMap.get(group.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + StringBuilder addressListSB = new StringBuilder(); + for (String item:registryList) { + addressListSB.append(item).append(","); + } + addressListStr = addressListSB.toString(); + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + group.setAddressList(addressListStr); + group.setUpdateTime(new Date()); + + XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group); + } + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + try { + TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop"); + } + }); + registryMonitorThread.setDaemon(true); + registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread"); + registryMonitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + registryOrRemoveThreadPool.shutdownNow(); + + // stop monitir (interrupt and wait) + registryMonitorThread.interrupt(); + try { + registryMonitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT<String> registry(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + public ReturnT<String> registryRemove(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue()); + if (ret > 0) { + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + private void freshGroupRegistryInfo(RegistryParam registryParam){ + // Under consideration, prevent affecting core tables + } + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java new file mode 100644 index 0000000..831bcf6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java @@ -0,0 +1,369 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author xuxueli 2019-05-21 + */ +public class JobScheduleHelper { + private static Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class); + + private static JobScheduleHelper instance = new JobScheduleHelper(); + public static JobScheduleHelper getInstance(){ + return instance; + } + + public static final long PRE_READ_MS = 5000; // pre read + + private Thread scheduleThread; + private Thread ringThread; + private volatile boolean scheduleThreadToStop = false; + private volatile boolean ringThreadToStop = false; + private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>(); + + public void start(){ + + // schedule thread + scheduleThread = new Thread(new Runnable() { + @Override + public void run() { + + try { + TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + logger.info(">>>>>>>>> init xxl-job admin scheduler success."); + + // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20) + int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; + + while (!scheduleThreadToStop) { + + // Scan Job + long start = System.currentTimeMillis(); + + Connection conn = null; + Boolean connAutoCommit = null; + PreparedStatement preparedStatement = null; + + boolean preReadSuc = true; + try { + + conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); + connAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); + preparedStatement.execute(); + + // tx start + + // 1、pre read + long nowTime = System.currentTimeMillis(); + List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount); + if (scheduleList!=null && scheduleList.size()>0) { + // 2、push time-ring + for (XxlJobInfo jobInfo: scheduleList) { + + // time-ring jump + if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) { + // 2.1、trigger-expire > 5s:pass && make next-trigger-time + logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId()); + + // 1、misfire match + MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); + if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) { + // FIRE_ONCE_NOW 》 trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + } + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + } else if (nowTime > jobInfo.getTriggerNextTime()) { + // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time + + // 1、trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + // next-trigger-time in 5s, pre-read again + if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) { + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } else { + // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } + + // 3、update trigger info + for (XxlJobInfo jobInfo: scheduleList) { + XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); + } + + } else { + preReadSuc = false; + } + + // tx stop + + + } catch (Exception e) { + if (!scheduleThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e); + } + } finally { + + // commit + if (conn != null) { + try { + conn.commit(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.setAutoCommit(connAutoCommit); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + // close PreparedStatement + if (null != preparedStatement) { + try { + preparedStatement.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + } + long cost = System.currentTimeMillis()-start; + + + // Wait seconds, align second + if (cost < 1000) { // scan-overtime, not wait + try { + // pre-read period: success > scan each second; fail > skip this period; + TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop"); + } + }); + scheduleThread.setDaemon(true); + scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread"); + scheduleThread.start(); + + + // ring thread + ringThread = new Thread(new Runnable() { + @Override + public void run() { + + while (!ringThreadToStop) { + + // align second + try { + TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + if (!ringThreadToStop) { + logger.error(e.getMessage(), e); + } + } + + try { + // second data + List<Integer> ringItemData = new ArrayList<>(); + int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长,跨过刻度,向前校验一个刻度; + for (int i = 0; i < 2; i++) { + List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 ); + if (tmpData != null) { + ringItemData.addAll(tmpData); + } + } + + // ring trigger + logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) ); + if (ringItemData.size() > 0) { + // do trigger + for (int jobId: ringItemData) { + // do trigger + JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null); + } + // clear + ringItemData.clear(); + } + } catch (Exception e) { + if (!ringThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop"); + } + }); + ringThread.setDaemon(true); + ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread"); + ringThread.start(); + } + + private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + Date nextValidTime = generateNextValidTime(jobInfo, fromTime); + if (nextValidTime != null) { + jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime()); + jobInfo.setTriggerNextTime(nextValidTime.getTime()); + } else { + jobInfo.setTriggerStatus(0); + jobInfo.setTriggerLastTime(0); + jobInfo.setTriggerNextTime(0); + logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}", + jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf()); + } + } + + private void pushTimeRing(int ringSecond, int jobId){ + // push async ring + List<Integer> ringItemData = ringData.get(ringSecond); + if (ringItemData == null) { + ringItemData = new ArrayList<Integer>(); + ringData.put(ringSecond, ringItemData); + } + ringItemData.add(jobId); + + logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) ); + } + + public void toStop(){ + + // 1、stop schedule + scheduleThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); // wait + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (scheduleThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + scheduleThread.interrupt(); + try { + scheduleThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // if has ring data + boolean hasRingData = false; + if (!ringData.isEmpty()) { + for (int second : ringData.keySet()) { + List<Integer> tmpData = ringData.get(second); + if (tmpData!=null && tmpData.size()>0) { + hasRingData = true; + break; + } + } + } + if (hasRingData) { + try { + TimeUnit.SECONDS.sleep(8); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // stop ring (wait job-in-memory stop) + ringThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (ringThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + ringThread.interrupt(); + try { + ringThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop"); + } + + + // ---------------------- tools ---------------------- + public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (ScheduleTypeEnum.CRON == scheduleTypeEnum) { + Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime); + return nextValidTime; + } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) { + return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 ); + } + return null; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java new file mode 100644 index 0000000..398713d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java @@ -0,0 +1,150 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.trigger.XxlJobTrigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * job trigger thread pool helper + * + * @author xuxueli 2018-07-03 21:08:07 + */ +public class JobTriggerPoolHelper { + private static Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class); + + + // ---------------------- trigger pool ---------------------- + + // fast/slow thread pool + private ThreadPoolExecutor fastTriggerPool = null; + private ThreadPoolExecutor slowTriggerPool = null; + + public void start(){ + fastTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(1000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()); + } + }); + + slowTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()); + } + }); + } + + + public void stop() { + //triggerPool.shutdown(); + fastTriggerPool.shutdownNow(); + slowTriggerPool.shutdownNow(); + logger.info(">>>>>>>>> xxl-job trigger thread pool shutdown success."); + } + + + // job timeout count + private volatile long minTim = System.currentTimeMillis()/60000; // ms > min + private volatile ConcurrentMap<Integer, AtomicInteger> jobTimeoutCountMap = new ConcurrentHashMap<>(); + + + /** + * add trigger + */ + public void addTrigger(final int jobId, + final TriggerTypeEnum triggerType, + final int failRetryCount, + final String executorShardingParam, + final String executorParam, + final String addressList) { + + // choose thread pool + ThreadPoolExecutor triggerPool_ = fastTriggerPool; + AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId); + if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job-timeout 10 times in 1 min + triggerPool_ = slowTriggerPool; + } + + // trigger + triggerPool_.execute(new Runnable() { + @Override + public void run() { + + long start = System.currentTimeMillis(); + + try { + // do trigger + XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + + // check timeout-count-map + long minTim_now = System.currentTimeMillis()/60000; + if (minTim != minTim_now) { + minTim = minTim_now; + jobTimeoutCountMap.clear(); + } + + // incr timeout-count-map + long cost = System.currentTimeMillis()-start; + if (cost > 500) { // ob-timeout threshold 500ms + AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1)); + if (timeoutCount != null) { + timeoutCount.incrementAndGet(); + } + } + + } + + } + }); + } + + + + // ---------------------- helper ---------------------- + + private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper(); + + public static void toStart() { + helper.start(); + } + public static void toStop() { + helper.stop(); + } + + /** + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + */ + public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { + helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java new file mode 100644 index 0000000..446c90e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java @@ -0,0 +1,27 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * trigger type enum + * + * @author xuxueli 2018-09-16 04:56:41 + */ +public enum TriggerTypeEnum { + + MANUAL(I18nUtil.getString("jobconf_trigger_type_manual")), + CRON(I18nUtil.getString("jobconf_trigger_type_cron")), + RETRY(I18nUtil.getString("jobconf_trigger_type_retry")), + PARENT(I18nUtil.getString("jobconf_trigger_type_parent")), + API(I18nUtil.getString("jobconf_trigger_type_api")), + MISFIRE(I18nUtil.getString("jobconf_trigger_type_misfire")); + + private TriggerTypeEnum(String title){ + this.title = title; + } + private String title; + public String getTitle() { + return title; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java new file mode 100644 index 0000000..748befc --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java @@ -0,0 +1,226 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.util.IpUtil; +import com.xxl.job.core.util.ThrowableUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * xxl-job trigger + * Created by xuxueli on 17/7/13. + */ +public class XxlJobTrigger { + private static Logger logger = LoggerFactory.getLogger(XxlJobTrigger.class); + + /** + * trigger job + * + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + * @param addressList + * null: use executor addressList + * not null: cover + */ + public static void trigger(int jobId, + TriggerTypeEnum triggerType, + int failRetryCount, + String executorShardingParam, + String executorParam, + String addressList) { + + // load data + XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId); + if (jobInfo == null) { + logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId); + return; + } + if (executorParam != null) { + jobInfo.setExecutorParam(executorParam); + } + int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount(); + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup()); + + // cover addressList + if (addressList!=null && addressList.trim().length()>0) { + group.setAddressType(1); + group.setAddressList(addressList.trim()); + } + + // sharding param + int[] shardingParam = null; + if (executorShardingParam!=null){ + String[] shardingArr = executorShardingParam.split("/"); + if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) { + shardingParam = new int[2]; + shardingParam[0] = Integer.valueOf(shardingArr[0]); + shardingParam[1] = Integer.valueOf(shardingArr[1]); + } + } + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) + && group.getRegistryList()!=null && !group.getRegistryList().isEmpty() + && shardingParam==null) { + for (int i = 0; i < group.getRegistryList().size(); i++) { + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size()); + } + } else { + if (shardingParam == null) { + shardingParam = new int[]{0, 1}; + } + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]); + } + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * @param group job group, registry list may be empty + * @param jobInfo + * @param finalFailRetryCount + * @param triggerType + * @param index sharding index + * @param total sharding index + */ + private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){ + + // param + ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy + ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy + String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null; + + // 1、save log-id + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setJobGroup(jobInfo.getJobGroup()); + jobLog.setJobId(jobInfo.getId()); + jobLog.setTriggerTime(new Date()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog); + logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId()); + + // 2、init trigger-param + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setJobId(jobInfo.getId()); + triggerParam.setExecutorHandler(jobInfo.getExecutorHandler()); + triggerParam.setExecutorParams(jobInfo.getExecutorParam()); + triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout()); + triggerParam.setLogId(jobLog.getId()); + triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime()); + triggerParam.setGlueType(jobInfo.getGlueType()); + triggerParam.setGlueSource(jobInfo.getGlueSource()); + triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime()); + triggerParam.setBroadcastIndex(index); + triggerParam.setBroadcastTotal(total); + + // 3、init address + String address = null; + ReturnT<String> routeAddressResult = null; + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { + if (index < group.getRegistryList().size()) { + address = group.getRegistryList().get(index); + } else { + address = group.getRegistryList().get(0); + } + } else { + routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList()); + if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) { + address = routeAddressResult.getContent(); + } + } + } else { + routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty")); + } + + // 4、trigger remote executor + ReturnT<String> triggerResult = null; + if (address != null) { + triggerResult = runExecutor(triggerParam, address); + } else { + triggerResult = new ReturnT<String>(ReturnT.FAIL_CODE, null); + } + + // 5、collection trigger info + StringBuffer triggerMsgSb = new StringBuffer(); + triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle()); + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp()); + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":") + .append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") ); + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList()); + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()); + if (shardingParam != null) { + triggerMsgSb.append("("+shardingParam+")"); + } + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle()); + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout()); + triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount); + + triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>") + .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"<br><br>":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():""); + + // 6、save log trigger-info + jobLog.setExecutorAddress(address); + jobLog.setExecutorHandler(jobInfo.getExecutorHandler()); + jobLog.setExecutorParam(jobInfo.getExecutorParam()); + jobLog.setExecutorShardingParam(shardingParam); + jobLog.setExecutorFailRetryCount(finalFailRetryCount); + //jobLog.setTriggerTime(); + jobLog.setTriggerCode(triggerResult.getCode()); + jobLog.setTriggerMsg(triggerMsgSb.toString()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog); + + logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId()); + } + + /** + * run executor + * @param triggerParam + * @param address + * @return + */ + public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){ + ReturnT<String> runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + runResult = executorBiz.run(triggerParam); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e); + runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e)); + } + + StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":"); + runResultSB.append("<br>address:").append(address); + runResultSB.append("<br>code:").append(runResult.getCode()); + runResultSB.append("<br>msg:").append(runResult.getMsg()); + + runResult.setMsg(runResultSB.toString()); + return runResult; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java new file mode 100644 index 0000000..a1523aa --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java @@ -0,0 +1,98 @@ +package com.xxl.job.admin.core.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Cookie.Util + * + * @author xuxueli 2015-12-12 18:01:06 + */ +public class CookieUtil { + + // 默认缓存时间,单位/秒, 2H + private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE; + // 保存路径,根路径 + private static final String COOKIE_PATH = "/"; + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param ifRemember + */ + public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) { + int age = ifRemember?COOKIE_MAX_AGE:-1; + set(response, key, value, null, COOKIE_PATH, age, true); + } + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param maxAge + */ + private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) { + Cookie cookie = new Cookie(key, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(isHttpOnly); + response.addCookie(cookie); + } + + /** + * 查询value + * + * @param request + * @param key + * @return + */ + public static String getValue(HttpServletRequest request, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + /** + * 查询Cookie + * + * @param request + * @param key + */ + private static Cookie get(HttpServletRequest request, String key) { + Cookie[] arr_cookie = request.getCookies(); + if (arr_cookie != null && arr_cookie.length > 0) { + for (Cookie cookie : arr_cookie) { + if (cookie.getName().equals(key)) { + return cookie; + } + } + } + return null; + } + + /** + * 删除Cookie + * + * @param request + * @param response + * @param key + */ + public static void remove(HttpServletRequest request, HttpServletResponse response, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + set(response, key, "", null, COOKIE_PATH, 0, true); + } + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java new file mode 100644 index 0000000..e90af43 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.core.util; + +import freemarker.ext.beans.BeansWrapper; +import freemarker.ext.beans.BeansWrapperBuilder; +import freemarker.template.Configuration; +import freemarker.template.TemplateHashModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ftl util + * + * @author xuxueli 2018-01-17 20:37:48 + */ +public class FtlUtil { + private static Logger logger = LoggerFactory.getLogger(FtlUtil.class); + + private static BeansWrapper wrapper = new BeansWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build(); //BeansWrapper.getDefaultInstance(); + + public static TemplateHashModel generateStaticModel(String packageName) { + try { + TemplateHashModel staticModels = wrapper.getStaticModels(); + TemplateHashModel fileStatics = (TemplateHashModel) staticModels.get(packageName); + return fileStatics; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java new file mode 100644 index 0000000..772a96e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.util; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * i18n util + * + * @author xuxueli 2018-01-17 20:39:06 + */ +public class I18nUtil { + private static Logger logger = LoggerFactory.getLogger(I18nUtil.class); + + private static Properties prop = null; + public static Properties loadI18nProp(){ + if (prop != null) { + return prop; + } + try { + // build i18n prop + String i18n = XxlJobAdminConfig.getAdminConfig().getI18n(); + String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n); + + // load prop + Resource resource = new ClassPathResource(i18nFile); + EncodedResource encodedResource = new EncodedResource(resource,"UTF-8"); + prop = PropertiesLoaderUtils.loadProperties(encodedResource); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return prop; + } + + /** + * get val of i18n key + * + * @param key + * @return + */ + public static String getString(String key) { + return loadI18nProp().getProperty(key); + } + + /** + * get mult val of i18n mult key, as json + * + * @param keys + * @return + */ + public static String getMultString(String... keys) { + Map<String, String> map = new HashMap<String, String>(); + + Properties prop = loadI18nProp(); + if (keys!=null && keys.length>0) { + for (String key: keys) { + map.put(key, prop.getProperty(key)); + } + } else { + for (String key: prop.stringPropertyNames()) { + map.put(key, prop.getProperty(key)); + } + } + + String json = JacksonUtil.writeValueAsString(map); + return json; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java new file mode 100644 index 0000000..4f4ea3c --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java @@ -0,0 +1,92 @@ +package com.xxl.job.admin.core.util; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Jackson util + * + * 1、obj need private and set/get; + * 2、do not support inner class; + * + * @author xuxueli 2015-9-25 18:02:56 + */ +public class JacksonUtil { + private static Logger logger = LoggerFactory.getLogger(JacksonUtil.class); + + private final static ObjectMapper objectMapper = new ObjectMapper(); + public static ObjectMapper getInstance() { + return objectMapper; + } + + /** + * bean、array、List、Map --> json + * + * @param obj + * @return json string + * @throws Exception + */ + public static String writeValueAsString(Object obj) { + try { + return getInstance().writeValueAsString(obj); + } catch (JsonGenerationException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> bean、Map、List(array) + * + * @param jsonStr + * @param clazz + * @return obj + * @throws Exception + */ + public static <T> T readValue(String jsonStr, Class<T> clazz) { + try { + return getInstance().readValue(jsonStr, clazz); + } catch (JsonParseException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> List<Bean>... + * + * @param jsonStr + * @param parametrized + * @param parameterClasses + * @param <T> + * @return + */ + public static <T> T readValue(String jsonStr, Class<?> parametrized, Class<?>... parameterClasses) { + try { + JavaType javaType = getInstance().getTypeFactory().constructParametricType(parametrized, parameterClasses); + return getInstance().readValue(jsonStr, javaType); + } catch (JsonParseException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java new file mode 100644 index 0000000..fbab061 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java @@ -0,0 +1,133 @@ +package com.xxl.job.admin.core.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * local cache tool + * + * @author xuxueli 2018-01-22 21:37:34 + */ +public class LocalCacheUtil { + + private static ConcurrentMap<String, LocalCacheData> cacheRepository = new ConcurrentHashMap<String, LocalCacheData>(); // 类型建议用抽象父类,兼容性更好; + private static class LocalCacheData{ + private String key; + private Object val; + private long timeoutTime; + + public LocalCacheData() { + } + + public LocalCacheData(String key, Object val, long timeoutTime) { + this.key = key; + this.val = val; + this.timeoutTime = timeoutTime; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Object getVal() { + return val; + } + + public void setVal(Object val) { + this.val = val; + } + + public long getTimeoutTime() { + return timeoutTime; + } + + public void setTimeoutTime(long timeoutTime) { + this.timeoutTime = timeoutTime; + } + } + + + /** + * set cache + * + * @param key + * @param val + * @param cacheTime + * @return + */ + public static boolean set(String key, Object val, long cacheTime){ + + // clean timeout cache, before set new cache (avoid cache too much) + cleanTimeoutCache(); + + // set new cache + if (key==null || key.trim().length()==0) { + return false; + } + if (val == null) { + remove(key); + } + if (cacheTime <= 0) { + remove(key); + } + long timeoutTime = System.currentTimeMillis() + cacheTime; + LocalCacheData localCacheData = new LocalCacheData(key, val, timeoutTime); + cacheRepository.put(localCacheData.getKey(), localCacheData); + return true; + } + + /** + * remove cache + * + * @param key + * @return + */ + public static boolean remove(String key){ + if (key==null || key.trim().length()==0) { + return false; + } + cacheRepository.remove(key); + return true; + } + + /** + * get cache + * + * @param key + * @return + */ + public static Object get(String key){ + if (key==null || key.trim().length()==0) { + return null; + } + LocalCacheData localCacheData = cacheRepository.get(key); + if (localCacheData!=null && System.currentTimeMillis()<localCacheData.getTimeoutTime()) { + return localCacheData.getVal(); + } else { + remove(key); + return null; + } + } + + /** + * clean timeout cache + * + * @return + */ + public static boolean cleanTimeoutCache(){ + if (!cacheRepository.keySet().isEmpty()) { + for (String key: cacheRepository.keySet()) { + LocalCacheData localCacheData = cacheRepository.get(key); + if (localCacheData!=null && System.currentTimeMillis()>=localCacheData.getTimeoutTime()) { + cacheRepository.remove(key); + } + } + } + return true; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java new file mode 100644 index 0000000..b608d9f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java @@ -0,0 +1,37 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobGroupDao { + + public List<XxlJobGroup> findAll(); + + public List<XxlJobGroup> findByAddressType(@Param("addressType") int addressType); + + public int save(XxlJobGroup xxlJobGroup); + + public int update(XxlJobGroup xxlJobGroup); + + public int remove(@Param("id") int id); + + public XxlJobGroup load(@Param("id") int id); + + public List<XxlJobGroup> pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("appname") String appname, + @Param("title") String title); + + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("appname") String appname, + @Param("title") String title); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java new file mode 100644 index 0000000..d640eff --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java @@ -0,0 +1,49 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +/** + * job info + * @author xuxueli 2016-1-12 18:03:45 + */ +@Mapper +public interface XxlJobInfoDao { + + public List<XxlJobInfo> pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("triggerStatus") int triggerStatus, + @Param("jobDesc") String jobDesc, + @Param("executorHandler") String executorHandler, + @Param("author") String author); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("triggerStatus") int triggerStatus, + @Param("jobDesc") String jobDesc, + @Param("executorHandler") String executorHandler, + @Param("author") String author); + + public int save(XxlJobInfo info); + + public XxlJobInfo loadById(@Param("id") int id); + + public int update(XxlJobInfo xxlJobInfo); + + public int delete(@Param("id") long id); + + public List<XxlJobInfo> getJobsByGroup(@Param("jobGroup") int jobGroup); + + public int findAllCount(); + + public List<XxlJobInfo> scheduleJobQuery(@Param("maxNextTime") long maxNextTime, @Param("pagesize") int pagesize ); + + public int scheduleUpdate(XxlJobInfo xxlJobInfo); + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java new file mode 100644 index 0000000..62fa3b4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java @@ -0,0 +1,62 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * job log + * @author xuxueli 2016-1-12 18:03:06 + */ +@Mapper +public interface XxlJobLogDao { + + // exist jobId not use jobGroup, not exist use jobGroup + public List<XxlJobLog> pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("triggerTimeStart") Date triggerTimeStart, + @Param("triggerTimeEnd") Date triggerTimeEnd, + @Param("logStatus") int logStatus); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("triggerTimeStart") Date triggerTimeStart, + @Param("triggerTimeEnd") Date triggerTimeEnd, + @Param("logStatus") int logStatus); + + public XxlJobLog load(@Param("id") long id); + + public long save(XxlJobLog xxlJobLog); + + public int updateTriggerInfo(XxlJobLog xxlJobLog); + + public int updateHandleInfo(XxlJobLog xxlJobLog); + + public int delete(@Param("jobId") int jobId); + + public Map<String, Object> findLogReport(@Param("from") Date from, + @Param("to") Date to); + + public List<Long> findClearLogIds(@Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("clearBeforeTime") Date clearBeforeTime, + @Param("clearBeforeNum") int clearBeforeNum, + @Param("pagesize") int pagesize); + public int clearLog(@Param("logIds") List<Long> logIds); + + public List<Long> findFailJobLogIds(@Param("pagesize") int pagesize); + + public int updateAlarmStatus(@Param("logId") long logId, + @Param("oldAlarmStatus") int oldAlarmStatus, + @Param("newAlarmStatus") int newAlarmStatus); + + public List<Long> findLostJobIds(@Param("losedTime") Date losedTime); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java new file mode 100644 index 0000000..3028aed --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * job log for glue + * @author xuxueli 2016-5-19 18:04:56 + */ +@Mapper +public interface XxlJobLogGlueDao { + + public int save(XxlJobLogGlue xxlJobLogGlue); + + public List<XxlJobLogGlue> findByJobId(@Param("jobId") int jobId); + + public int removeOld(@Param("jobId") int jobId, @Param("limit") int limit); + + public int deleteByJobId(@Param("jobId") int jobId); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java new file mode 100644 index 0000000..f4b3dc8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java @@ -0,0 +1,26 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * job log + * @author xuxueli 2019-11-22 + */ +@Mapper +public interface XxlJobLogReportDao { + + public int save(XxlJobLogReport xxlJobLogReport); + + public int update(XxlJobLogReport xxlJobLogReport); + + public List<XxlJobLogReport> queryLogReport(@Param("triggerDayFrom") Date triggerDayFrom, + @Param("triggerDayTo") Date triggerDayTo); + + public XxlJobLogReport queryLogReportTotal(); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java new file mode 100644 index 0000000..1005c46 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java @@ -0,0 +1,38 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobRegistry; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobRegistryDao { + + public List<Integer> findDead(@Param("timeout") int timeout, + @Param("nowTime") Date nowTime); + + public int removeDead(@Param("ids") List<Integer> ids); + + public List<XxlJobRegistry> findAll(@Param("timeout") int timeout, + @Param("nowTime") Date nowTime); + + public int registryUpdate(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registrySave(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registryDelete(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java new file mode 100644 index 0000000..e840494 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * @author xuxueli 2019-05-04 16:44:59 + */ +@Mapper +public interface XxlJobUserDao { + + public List<XxlJobUser> pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + + public XxlJobUser loadByUserName(@Param("username") String username); + + public int save(XxlJobUser xxlJobUser); + + public int update(XxlJobUser xxlJobUser); + + public int delete(@Param("id") int id); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java new file mode 100644 index 0000000..e1cf2e4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java @@ -0,0 +1,107 @@ +package com.xxl.job.admin.service; + +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.CookieUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.core.util.JacksonUtil; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.DigestUtils; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.math.BigInteger; + +/** + * @author xuxueli 2019-05-04 22:13:264 + */ +@Configuration +public class LoginService { + + public static final String LOGIN_IDENTITY_KEY = "XXL_JOB_LOGIN_IDENTITY"; + + @Resource + private XxlJobUserDao xxlJobUserDao; + + + private String makeToken(XxlJobUser xxlJobUser){ + String tokenJson = JacksonUtil.writeValueAsString(xxlJobUser); + String tokenHex = new BigInteger(tokenJson.getBytes()).toString(16); + return tokenHex; + } + private XxlJobUser parseToken(String tokenHex){ + XxlJobUser xxlJobUser = null; + if (tokenHex != null) { + String tokenJson = new String(new BigInteger(tokenHex, 16).toByteArray()); // username_password(md5) + xxlJobUser = JacksonUtil.readValue(tokenJson, XxlJobUser.class); + } + return xxlJobUser; + } + + + public ReturnT<String> login(HttpServletRequest request, HttpServletResponse response, String username, String password, boolean ifRemember){ + + // param + if (username==null || username.trim().length()==0 || password==null || password.trim().length()==0){ + return new ReturnT<String>(500, I18nUtil.getString("login_param_empty")); + } + + // valid passowrd + XxlJobUser xxlJobUser = xxlJobUserDao.loadByUserName(username); + if (xxlJobUser == null) { + return new ReturnT<String>(500, I18nUtil.getString("login_param_unvalid")); + } + String passwordMd5 = DigestUtils.md5DigestAsHex(password.getBytes()); + if (!passwordMd5.equals(xxlJobUser.getPassword())) { + return new ReturnT<String>(500, I18nUtil.getString("login_param_unvalid")); + } + + String loginToken = makeToken(xxlJobUser); + + // do login + CookieUtil.set(response, LOGIN_IDENTITY_KEY, loginToken, ifRemember); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @param response + */ + public ReturnT<String> logout(HttpServletRequest request, HttpServletResponse response){ + CookieUtil.remove(request, response, LOGIN_IDENTITY_KEY); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @return + */ + public XxlJobUser ifLogin(HttpServletRequest request, HttpServletResponse response){ + String cookieToken = CookieUtil.getValue(request, LOGIN_IDENTITY_KEY); + if (cookieToken != null) { + XxlJobUser cookieUser = null; + try { + cookieUser = parseToken(cookieToken); + } catch (Exception e) { + logout(request, response); + } + if (cookieUser != null) { + XxlJobUser dbUser = xxlJobUserDao.loadByUserName(cookieUser.getUsername()); + if (dbUser != null) { + if (cookieUser.getPassword().equals(dbUser.getPassword())) { + return dbUser; + } + } + } + } + return null; + } + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java new file mode 100644 index 0000000..61da3a2 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java @@ -0,0 +1,86 @@ +package com.xxl.job.admin.service; + + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.core.biz.model.ReturnT; + +import java.util.Date; +import java.util.Map; + +/** + * core job action for xxl-job + * + * @author xuxueli 2016-5-28 15:30:33 + */ +public interface XxlJobService { + + /** + * page list + * + * @param start + * @param length + * @param jobGroup + * @param jobDesc + * @param executorHandler + * @param author + * @return + */ + public Map<String, Object> pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author); + + /** + * add job + * + * @param jobInfo + * @return + */ + public ReturnT<String> add(XxlJobInfo jobInfo); + + /** + * update job + * + * @param jobInfo + * @return + */ + public ReturnT<String> update(XxlJobInfo jobInfo); + + /** + * remove job + * * + * @param id + * @return + */ + public ReturnT<String> remove(int id); + + /** + * start job + * + * @param id + * @return + */ + public ReturnT<String> start(int id); + + /** + * stop job + * + * @param id + * @return + */ + public ReturnT<String> stop(int id); + + /** + * dashboard info + * + * @return + */ + public Map<String,Object> dashboardInfo(); + + /** + * chart info + * + * @param startDate + * @param endDate + * @return + */ + public ReturnT<Map<String,Object>> chartInfo(Date startDate, Date endDate); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java new file mode 100644 index 0000000..3c01e94 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java @@ -0,0 +1,35 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.thread.JobCompleteHelper; +import com.xxl.job.admin.core.thread.JobRegistryHelper; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author xuxueli 2017-07-27 21:54:20 + */ +@Service +public class AdminBizImpl implements AdminBiz { + + + @Override + public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) { + return JobCompleteHelper.getInstance().callback(callbackParamList); + } + + @Override + public ReturnT<String> registry(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registry(registryParam); + } + + @Override + public ReturnT<String> registryRemove(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registryRemove(registryParam); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java new file mode 100644 index 0000000..530ee41 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java @@ -0,0 +1,434 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.*; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.text.MessageFormat; +import java.util.*; + +/** + * core job action for xxl-job + * @author xuxueli 2016-5-28 15:30:33 + */ +@Service +public class XxlJobServiceImpl implements XxlJobService { + private static Logger logger = LoggerFactory.getLogger(XxlJobServiceImpl.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + + @Override + public Map<String, Object> pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + // page list + List<XxlJobInfo> list = xxlJobInfoDao.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + int list_count = xxlJobInfoDao.pageListCount(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + + // package result + Map<String, Object> maps = new HashMap<String, Object>(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @Override + public ReturnT<String> add(XxlJobInfo jobInfo) { + + // valid base + XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (group == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")) ); + } + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid")); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid job + if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) ); + } + if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+"JobHandler") ); + } + // 》fix "\r" in shell + if (GlueTypeEnum.GLUE_SHELL==GlueTypeEnum.match(jobInfo.getGlueType()) && jobInfo.getGlueSource()!=null) { + jobInfo.setGlueSource(jobInfo.getGlueSource().replaceAll("\r", "")); + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT<String>(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // add in db + jobInfo.setAddTime(new Date()); + jobInfo.setUpdateTime(new Date()); + jobInfo.setGlueUpdatetime(new Date()); + xxlJobInfoDao.save(jobInfo); + if (jobInfo.getId() < 1) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) ); + } + + return new ReturnT<String>(String.valueOf(jobInfo.getId())); + } + + private boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + public ReturnT<String> update(XxlJobInfo jobInfo) { + + // valid base + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid") ); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE /*|| scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT<String>(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // group valid + XxlJobGroup jobGroup = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (jobGroup == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_jobgroup")+I18nUtil.getString("system_unvalid")) ); + } + + // stage job info + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId()); + if (exists_jobInfo == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = exists_jobInfo.getTriggerNextTime(); + boolean scheduleDataNotChanged = jobInfo.getScheduleType().equals(exists_jobInfo.getScheduleType()) && jobInfo.getScheduleConf().equals(exists_jobInfo.getScheduleConf()); + if (exists_jobInfo.getTriggerStatus() == 1 && !scheduleDataNotChanged) { + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(jobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + exists_jobInfo.setJobGroup(jobInfo.getJobGroup()); + exists_jobInfo.setJobDesc(jobInfo.getJobDesc()); + exists_jobInfo.setAuthor(jobInfo.getAuthor()); + exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail()); + exists_jobInfo.setScheduleType(jobInfo.getScheduleType()); + exists_jobInfo.setScheduleConf(jobInfo.getScheduleConf()); + exists_jobInfo.setMisfireStrategy(jobInfo.getMisfireStrategy()); + exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy()); + exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler()); + exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam()); + exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout()); + exists_jobInfo.setExecutorFailRetryCount(jobInfo.getExecutorFailRetryCount()); + exists_jobInfo.setChildJobId(jobInfo.getChildJobId()); + exists_jobInfo.setTriggerNextTime(nextTriggerTime); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + + return ReturnT.SUCCESS; + } + + @Override + public ReturnT<String> remove(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + if (xxlJobInfo == null) { + return ReturnT.SUCCESS; + } + + xxlJobInfoDao.delete(id); + xxlJobLogDao.delete(id); + xxlJobLogGlueDao.deleteByJobId(id); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT<String> start(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + // valid + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE); + if (ScheduleTypeEnum.NONE == scheduleTypeEnum) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type_none_limit_start")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = 0; + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(xxlJobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + + xxlJobInfo.setTriggerStatus(1); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(nextTriggerTime); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT<String> stop(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + xxlJobInfo.setTriggerStatus(0); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(0); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + @Override + public Map<String, Object> dashboardInfo() { + + int jobInfoCount = xxlJobInfoDao.findAllCount(); + int jobLogCount = 0; + int jobLogSuccessCount = 0; + XxlJobLogReport xxlJobLogReport = xxlJobLogReportDao.queryLogReportTotal(); + if (xxlJobLogReport != null) { + jobLogCount = xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount(); + jobLogSuccessCount = xxlJobLogReport.getSucCount(); + } + + // executor count + Set<String> executorAddressSet = new HashSet<String>(); + List<XxlJobGroup> groupList = xxlJobGroupDao.findAll(); + + if (groupList!=null && !groupList.isEmpty()) { + for (XxlJobGroup group: groupList) { + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + executorAddressSet.addAll(group.getRegistryList()); + } + } + } + + int executorCount = executorAddressSet.size(); + + Map<String, Object> dashboardMap = new HashMap<String, Object>(); + dashboardMap.put("jobInfoCount", jobInfoCount); + dashboardMap.put("jobLogCount", jobLogCount); + dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount); + dashboardMap.put("executorCount", executorCount); + return dashboardMap; + } + + @Override + public ReturnT<Map<String, Object>> chartInfo(Date startDate, Date endDate) { + + // process + List<String> triggerDayList = new ArrayList<String>(); + List<Integer> triggerDayCountRunningList = new ArrayList<Integer>(); + List<Integer> triggerDayCountSucList = new ArrayList<Integer>(); + List<Integer> triggerDayCountFailList = new ArrayList<Integer>(); + int triggerCountRunningTotal = 0; + int triggerCountSucTotal = 0; + int triggerCountFailTotal = 0; + + List<XxlJobLogReport> logReportList = xxlJobLogReportDao.queryLogReport(startDate, endDate); + + if (logReportList!=null && logReportList.size()>0) { + for (XxlJobLogReport item: logReportList) { + String day = DateUtil.formatDate(item.getTriggerDay()); + int triggerDayCountRunning = item.getRunningCount(); + int triggerDayCountSuc = item.getSucCount(); + int triggerDayCountFail = item.getFailCount(); + + triggerDayList.add(day); + triggerDayCountRunningList.add(triggerDayCountRunning); + triggerDayCountSucList.add(triggerDayCountSuc); + triggerDayCountFailList.add(triggerDayCountFail); + + triggerCountRunningTotal += triggerDayCountRunning; + triggerCountSucTotal += triggerDayCountSuc; + triggerCountFailTotal += triggerDayCountFail; + } + } else { + for (int i = -6; i <= 0; i++) { + triggerDayList.add(DateUtil.formatDate(DateUtil.addDays(new Date(), i))); + triggerDayCountRunningList.add(0); + triggerDayCountSucList.add(0); + triggerDayCountFailList.add(0); + } + } + + Map<String, Object> result = new HashMap<String, Object>(); + result.put("triggerDayList", triggerDayList); + result.put("triggerDayCountRunningList", triggerDayCountRunningList); + result.put("triggerDayCountSucList", triggerDayCountSucList); + result.put("triggerDayCountFailList", triggerDayCountFailList); + + result.put("triggerCountRunningTotal", triggerCountRunningTotal); + result.put("triggerCountSucTotal", triggerCountSucTotal); + result.put("triggerCountFailTotal", triggerCountFailTotal); + + return new ReturnT<Map<String, Object>>(result); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/resources/application.properties b/xxl-job/xxl-job-admin/src/main/resources/application.properties new file mode 100644 index 0000000..9e29565 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/application.properties @@ -0,0 +1,65 @@ +### web +server.port=8080 +server.servlet.context-path=/xxl-job-admin + +### actuator +management.server.servlet.context-path=/actuator +management.health.mail.enabled=false + +### resources +spring.mvc.servlet.load-on-startup=0 +spring.mvc.static-path-pattern=/static/** +spring.resources.static-locations=classpath:/static/ + +### freemarker +spring.freemarker.templateLoaderPath=classpath:/templates/ +spring.freemarker.suffix=.ftl +spring.freemarker.charset=UTF-8 +spring.freemarker.request-context-attribute=request +spring.freemarker.settings.number_format=0.########## + +### mybatis +mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml +#mybatis.type-aliases-package=com.xxl.job.admin.core.model + +### xxl-job, datasource +spring.datasource.url=jdbc:mysql://rm-j6cwl6y72ln2918q2.mysql.rds.aliyuncs.com:3306/cdn1?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true +spring.datasource.username=root +spring.datasource.password=!QAse4#@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +### datasource-pool +spring.datasource.type=com.zaxxer.hikari.HikariDataSource +spring.datasource.hikari.minimum-idle=10 +spring.datasource.hikari.maximum-pool-size=30 +spring.datasource.hikari.auto-commit=true +spring.datasource.hikari.idle-timeout=30000 +spring.datasource.hikari.pool-name=HikariCP +spring.datasource.hikari.max-lifetime=900000 +spring.datasource.hikari.connection-timeout=10000 +spring.datasource.hikari.connection-test-query=SELECT 1 +spring.datasource.hikari.validation-timeout=1000 + +### xxl-job, email +spring.mail.host=smtp.qq.com +spring.mail.port=25 +spring.mail.username=xxx@qq.com +spring.mail.from=xxx@qq.com +spring.mail.password=xxx +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true +spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory + +### xxl-job, access token +xxl.job.accessToken=default_token + +### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en") +xxl.job.i18n=zh_CN + +## xxl-job, triggerpool max size +xxl.job.triggerpool.fast.max=200 +xxl.job.triggerpool.slow.max=100 + +### xxl-job, log retention days +xxl.job.logretentiondays=30 diff --git a/xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties new file mode 100644 index 0000000..001f841 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties @@ -0,0 +1,276 @@ +admin_name=Scheduling Center +admin_name_full=Distributed Task Scheduling Platform XXL-JOB +admin_version=2.3.1 +admin_i18n=en + +## system +system_tips=System message +system_ok=Confirm +system_close=Close +system_save=Save +system_cancel=Cancel +system_search=Search +system_status=Status +system_opt=Operate +system_please_input=please input +system_please_choose=please choose +system_success=success +system_fail=fail +system_add_suc=add success +system_add_fail=add fail +system_update_suc=update success +system_update_fail=update fail +system_all=All +system_api_error=net error +system_show=Show +system_empty=Empty +system_opt_suc=operate success +system_opt_fail=operate fail +system_opt_edit=Edit +system_opt_del=Delete +system_opt_copy=Copy +system_unvalid=illegal +system_not_found=not exist +system_nav=Navigation +system_digits=digits +system_lengh_limit=Length limit +system_permission_limit=Permission limit +system_welcome=Welcome + +## daterangepicker +daterangepicker_ranges_recent_hour=recent one hour +daterangepicker_ranges_today=today +daterangepicker_ranges_yesterday=yesterday +daterangepicker_ranges_this_month=this month +daterangepicker_ranges_last_month=last month +daterangepicker_ranges_recent_week=recent one week +daterangepicker_ranges_recent_month=recent one month +daterangepicker_custom_name=custom +daterangepicker_custom_starttime=start time +daterangepicker_custom_endtime=end time +daterangepicker_custom_daysofweek=Sun,Mon,Tue,Wed,Thu,Fri,Sat +daterangepicker_custom_monthnames=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec + +## dataTable +dataTable_sProcessing=processing... +dataTable_sLengthMenu= _MENU_ records per page +dataTable_sZeroRecords=No matching results +dataTable_sInfo=page _PAGE_ ( Total _PAGES_ pages,_TOTAL_ records ) +dataTable_sInfoEmpty=No Record +dataTable_sInfoFiltered=(Filtered by _MAX_ results) +dataTable_sSearch=Search +dataTable_sEmptyTable=Table data is empty +dataTable_sLoadingRecords=Loading... +dataTable_sFirst=FIRST PAGE +dataTable_sPrevious=Previous Page +dataTable_sNext=Next Page +dataTable_sLast=LAST PAGE +dataTable_sSortAscending=: Rank this column in ascending order +dataTable_sSortDescending=: Rank this column in descending order + +## login +login_btn=Login +login_remember_me=Remember Me +login_username_placeholder=Please enter username +login_password_placeholder=Please enter password +login_username_empty=Please enter username +login_username_lt_4=Username length should not be less than 4 +login_password_empty=Please enter password +login_password_lt_4=Password length should not be less than 4 +login_success=Login success +login_fail=Login fail +login_param_empty=Username or password is empty +login_param_unvalid=Username or password error + +## logout +logout_btn=Logout +logout_confirm=Confirm logout? +logout_success=Logout success +logout_fail=Logout fail + +## change pwd +change_pwd=Change password +change_pwd_suc_to_logout=Change password successful, about to log out login +change_pwd_field_newpwd=new password + +## dashboard +job_dashboard_name=Run report +job_dashboard_job_num=Job number +job_dashboard_job_num_tip=The number of tasks running in the scheduling center +job_dashboard_trigger_num=trigger number +job_dashboard_trigger_num_tip=The number of trigger record scheduled by the scheduling center +job_dashboard_jobgroup_num=Executor number +job_dashboard_jobgroup_num_tip=The number of online executor machines perceived by the scheduling center +job_dashboard_report=Scheduling report +job_dashboard_report_loaddata_fail=Scheduling report load data error +job_dashboard_date_report=Date distribution +job_dashboard_rate_report=Percentage distribution + +## job info +jobinfo_name=Job Manage +jobinfo_job=Job +jobinfo_field_add=Add Job +jobinfo_field_update=Edit Job +jobinfo_field_id=Job ID +jobinfo_field_jobgroup=Executor +jobinfo_field_jobdesc=Job description +jobinfo_field_timeout=Job timeout period +jobinfo_field_gluetype=GLUE Type +jobinfo_field_executorparam=Param +jobinfo_field_author=Author +jobinfo_field_alarmemail=Alarm email +jobinfo_field_alarmemail_placeholder=Please enter alarm mail, if there are more than one comma separated +jobinfo_field_executorRouteStrategy=Route Strategy +jobinfo_field_childJobId=Child Job ID +jobinfo_field_childJobId_placeholder=Please enter the Child job ID, if there are more than one comma separated +jobinfo_field_executorBlockStrategy=Block Strategy +jobinfo_field_executorFailRetryCount=Fail Retry Count +jobinfo_field_executorFailRetryCount_placeholder=Fail Retry Count. effect if greater than zero +jobinfo_script_location=Script location +jobinfo_shard_index=Shard index +jobinfo_shard_total=Shard total +jobinfo_opt_stop=Stop +jobinfo_opt_start=Start +jobinfo_opt_log=Query Log +jobinfo_opt_run=Run Once +jobinfo_opt_run_tips=Please input the address for this trigger. Null will be obtained from the executor +jobinfo_opt_registryinfo=Registry Info +jobinfo_opt_next_time=Next trigger time +jobinfo_glue_remark=Resource Remark +jobinfo_glue_remark_limit=Resource Remark length is limited to 4~100 +jobinfo_glue_rollback=Version Backtrack +jobinfo_glue_jobid_unvalid=Job ID is illegal +jobinfo_glue_gluetype_unvalid=The job is not GLUE Type +jobinfo_field_executorTimeout_placeholder=Job Timeout period,in seconds. effect if greater than zero +schedule_type=Schedule Type +schedule_type_none=None +schedule_type_cron=Cron +schedule_type_fix_rate=Fix rate +schedule_type_fix_delay=Fix delay +schedule_type_none_limit_start=The current schedule type disables startup +misfire_strategy=Misfire strategy +misfire_strategy_do_nothing=Do nothing +misfire_strategy_fire_once_now=Fire once now +jobinfo_conf_base=Base configuration +jobinfo_conf_schedule=Schedule configuration +jobinfo_conf_job=Job configuration +jobinfo_conf_advanced=Advanced configuration + +## job log +joblog_name=Trigger Log +joblog_status=Status +joblog_status_all=All +joblog_status_suc=Success +joblog_status_fail=Fail +joblog_status_running=Running +joblog_field_triggerTime=Trigger Time +joblog_field_triggerCode=Trigger Result +joblog_field_triggerMsg=Trigger Msg +joblog_field_handleTime=Handle Time +joblog_field_handleCode=Handle Result +joblog_field_handleMsg=Trigger Msg +joblog_field_executorAddress=Executor Address +joblog_clean=Clean +joblog_clean_log=Clean Log +joblog_clean_type=Clean Type +joblog_clean_type_1=Clean up log data a month ago +joblog_clean_type_2=Clean up log data three month ago +joblog_clean_type_3=Clean up log data six month ago +joblog_clean_type_4=Clean up log data a year ago +joblog_clean_type_5=Clean up log data a thousand record ago +joblog_clean_type_6=Clean up log data ten thousand record ago +joblog_clean_type_7=Clean up log data thirty thousand record ago +joblog_clean_type_8=Clean up log data hundred thousand record ago +joblog_clean_type_9=Clean up all log data +joblog_clean_type_unvalid=Clean type is illegal +joblog_handleCode_200=Success +joblog_handleCode_500=Fail +joblog_handleCode_502=Timeout +joblog_kill_log=Kill Job +joblog_kill_log_limit=Trigger Fail, can not kill job +joblog_kill_log_byman=Manual operation, kill job +joblog_lost_fail=Job result lost, marked as failure +joblog_rolling_log=Rolling log +joblog_rolling_log_refresh=Refresh +joblog_rolling_log_triggerfail=The job trigger fail, can not view the rolling log +joblog_rolling_log_failoften=The request for the Rolling log is terminated, the number of failed requests exceeds the limit, Reload the log on the refresh page +joblog_logid_unvalid=Log ID is illegal + +## job group +jobgroup_name=Executor Manage +jobgroup_list=Executor List +jobgroup_add=Add Executor +jobgroup_edit=Edit Executor +jobgroup_del=Delete Executor +jobgroup_field_title=Title +jobgroup_field_addressType=Registry Type +jobgroup_field_addressType_0=Automatic registration +jobgroup_field_addressType_1=Manual registration +jobgroup_field_addressType_limit=Manually registration type, the machine address must not be empty +jobgroup_field_registryList=machine address +jobgroup_field_registryList_unvalid=registry machine address is illegal +jobgroup_field_registryList_placeholder=Please enter the machine address, if there are more than one comma separated +jobgroup_field_appname_limit=Limit the beginning of a lowercase letter, consists of lowercase letters、number and hyphen. +jobgroup_field_appname_length=AppName length is limited to 4~64 +jobgroup_field_title_length=Title length is limited to 4~12 +jobgroup_field_order_digits=Please enter a positive integer +jobgroup_field_orderrange=Order is limited to 1~1000 +jobgroup_del_limit_0=Refuse to delete, the executor is being used +jobgroup_del_limit_1=Refuses to delete, the system retains at least one executor +jobgroup_empty=There is no valid executor. Please contact the administrator + +## job conf +jobconf_block_SERIAL_EXECUTION=Serial execution +jobconf_block_DISCARD_LATER=Discard Later +jobconf_block_COVER_EARLY=Cover Early +jobconf_route_first=First +jobconf_route_last=Last +jobconf_route_round=Round +jobconf_route_random=Random +jobconf_route_consistenthash=Consistent Hash +jobconf_route_lfu=Least Frequently Used +jobconf_route_lru=Least Recently Used +jobconf_route_failover=Failover +jobconf_route_busyover=Busyover +jobconf_route_shard=Sharding Broadcast +jobconf_idleBeat=Idle check +jobconf_beat=Heartbeats +jobconf_monitor=Task Scheduling Center monitor alarm +jobconf_monitor_detail=monitor alarm details +jobconf_monitor_alarm_title=Alarm Type +jobconf_monitor_alarm_type=Trigger Fail +jobconf_monitor_alarm_content=Alarm Content +jobconf_trigger_admin_adress=Trigger machine address +jobconf_trigger_exe_regtype=Execotor-Registry Type +jobconf_trigger_exe_regaddress=Execotor-Registry Address +jobconf_trigger_address_empty=Trigger Fail:registry address is empty +jobconf_trigger_run=Trigger Job +jobconf_trigger_child_run=Trigger child job +jobconf_callback_child_msg1={0}/{1} [Job ID={2}], Trigger {3}, Trigger msg: {4} <br> +jobconf_callback_child_msg2={0}/{1} [Job ID={2}], Trigger Fail, Trigger msg: Job ID is illegal <br> +jobconf_trigger_type=Job trigger type +jobconf_trigger_type_cron=Cron trigger +jobconf_trigger_type_manual=Manual trigger +jobconf_trigger_type_parent=Parent job trigger +jobconf_trigger_type_api=Api trigger +jobconf_trigger_type_retry=Fail retry trigger +jobconf_trigger_type_misfire=Misfire compensation trigger + +## user +user_manage=User Manage +user_username=Username +user_password=Password +user_role=Role +user_role_admin=Admin User +user_role_normal=Normal User +user_permission=Permission +user_add=Add User +user_update=Edit User +user_username_repeat=Username Repeat +user_username_valid=Restrictions start with a lowercase letter and consist of lowercase letters and Numbers +user_password_update_placeholder=Please input password, empty means not update +user_update_loginuser_limit=Operation of current login account is not allowed + +## help +job_help=Tutorial +job_help_document=Official Document diff --git a/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties new file mode 100644 index 0000000..69ef6e3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties @@ -0,0 +1,276 @@ +admin_name=任务调度中心 +admin_name_full=分布式任务调度平台XXL-JOB +admin_version=2.3.1 +admin_i18n= + +## system +system_tips=系统提示 +system_ok=确定 +system_close=关闭 +system_save=保存 +system_cancel=取消 +system_search=搜索 +system_status=状态 +system_opt=操作 +system_please_input=请输入 +system_please_choose=请选择 +system_success=成功 +system_fail=失败 +system_add_suc=新增成功 +system_add_fail=新增失败 +system_update_suc=更新成功 +system_update_fail=更新失败 +system_all=全部 +system_api_error=接口异常 +system_show=查看 +system_empty=无 +system_opt_suc=操作成功 +system_opt_fail=操作失败 +system_opt_edit=编辑 +system_opt_del=删除 +system_opt_copy=复制 +system_unvalid=非法 +system_not_found=不存在 +system_nav=导航 +system_digits=整数 +system_lengh_limit=长度限制 +system_permission_limit=权限拦截 +system_welcome=欢迎 + +## daterangepicker +daterangepicker_ranges_recent_hour=最近一小时 +daterangepicker_ranges_today=今日 +daterangepicker_ranges_yesterday=昨日 +daterangepicker_ranges_this_month=本月 +daterangepicker_ranges_last_month=上个月 +daterangepicker_ranges_recent_week=最近一周 +daterangepicker_ranges_recent_month=最近一月 +daterangepicker_custom_name=自定义 +daterangepicker_custom_starttime=起始时间 +daterangepicker_custom_endtime=结束时间 +daterangepicker_custom_daysofweek=日,一,二,三,四,五,六 +daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing=处理中... +dataTable_sLengthMenu=每页 _MENU_ 条记录 +dataTable_sZeroRecords=没有匹配结果 +dataTable_sInfo=第 _PAGE_ 页 ( 总共 _PAGES_ 页,_TOTAL_ 条记录 ) +dataTable_sInfoEmpty=无记录 +dataTable_sInfoFiltered=(由 _MAX_ 项结果过滤) +dataTable_sSearch=搜索 +dataTable_sEmptyTable=表中数据为空 +dataTable_sLoadingRecords=载入中... +dataTable_sFirst=首页 +dataTable_sPrevious=上页 +dataTable_sNext=下页 +dataTable_sLast=末页 +dataTable_sSortAscending=: 以升序排列此列 +dataTable_sSortDescending=: 以降序排列此列 + +## login +login_btn=登录 +login_remember_me=记住密码 +login_username_placeholder=请输入登录账号 +login_password_placeholder=请输入登录密码 +login_username_empty=请输入登录账号 +login_username_lt_4=登录账号不应低于4位 +login_password_empty=请输入登录密码 +login_password_lt_4=登录密码不应低于4位 +login_success=登录成功 +login_fail=登录失败 +login_param_empty=账号或密码为空 +login_param_unvalid=账号或密码错误 + +## logout +logout_btn=注销 +logout_confirm=确认注销登录? +logout_success=注销成功 +logout_fail=注销失败 + +## change pwd +change_pwd=修改密码 +change_pwd_suc_to_logout=修改密码成功,即将注销登陆 +change_pwd_field_newpwd=新密码 + +## dashboard +job_dashboard_name=运行报表 +job_dashboard_job_num=任务数量 +job_dashboard_job_num_tip=调度中心运行的任务数量 +job_dashboard_trigger_num=调度次数 +job_dashboard_trigger_num_tip=调度中心触发的调度次数 +job_dashboard_jobgroup_num=执行器数量 +job_dashboard_jobgroup_num_tip=调度中心在线的执行器机器数量 +job_dashboard_report=调度报表 +job_dashboard_report_loaddata_fail=调度报表数据加载异常 +job_dashboard_date_report=日期分布图 +job_dashboard_rate_report=成功比例图 + +## job info +jobinfo_name=任务管理 +jobinfo_job=任务 +jobinfo_field_add=新增 +jobinfo_field_update=更新任务 +jobinfo_field_id=任务ID +jobinfo_field_jobgroup=执行器 +jobinfo_field_jobdesc=任务描述 +jobinfo_field_gluetype=运行模式 +jobinfo_field_executorparam=任务参数 +jobinfo_field_author=负责人 +jobinfo_field_timeout=任务超时时间 +jobinfo_field_alarmemail=报警邮件 +jobinfo_field_alarmemail_placeholder=请输入报警邮件,多个邮件地址则逗号分隔 +jobinfo_field_executorRouteStrategy=路由策略 +jobinfo_field_childJobId=子任务ID +jobinfo_field_childJobId_placeholder=请输入子任务的任务ID,如存在多个则逗号分隔 +jobinfo_field_executorBlockStrategy=阻塞处理策略 +jobinfo_field_executorFailRetryCount=失败重试次数 +jobinfo_field_executorFailRetryCount_placeholder=失败重试次数,大于零时生效 +jobinfo_script_location=脚本位置 +jobinfo_shard_index=分片序号 +jobinfo_shard_total=分片总数 +jobinfo_opt_stop=停止 +jobinfo_opt_start=启动 +jobinfo_opt_log=查询日志 +jobinfo_opt_run=执行一次 +jobinfo_opt_run_tips=请输入本次执行的机器地址,为空则从执行器获取 +jobinfo_opt_registryinfo=注册节点 +jobinfo_opt_next_time=下次执行时间 +jobinfo_glue_remark=源码备注 +jobinfo_glue_remark_limit=源码备注长度限制为4~100 +jobinfo_glue_rollback=版本回溯 +jobinfo_glue_jobid_unvalid=任务ID非法 +jobinfo_glue_gluetype_unvalid=该任务非GLUE模式 +jobinfo_field_executorTimeout_placeholder=任务超时时间,单位秒,大于零时生效 +schedule_type=调度类型 +schedule_type_none=无 +schedule_type_cron=CRON +schedule_type_fix_rate=固定速度 +schedule_type_fix_delay=固定延迟 +schedule_type_none_limit_start=当前调度类型禁止启动 +misfire_strategy=调度过期策略 +misfire_strategy_do_nothing=忽略 +misfire_strategy_fire_once_now=立即执行一次 +jobinfo_conf_base=基础配置 +jobinfo_conf_schedule=调度配置 +jobinfo_conf_job=任务配置 +jobinfo_conf_advanced=高级配置 + +## job log +joblog_name=调度日志 +joblog_status=状态 +joblog_status_all=全部 +joblog_status_suc=成功 +joblog_status_fail=失败 +joblog_status_running=进行中 +joblog_field_triggerTime=调度时间 +joblog_field_triggerCode=调度结果 +joblog_field_triggerMsg=调度备注 +joblog_field_handleTime=执行时间 +joblog_field_handleCode=执行结果 +joblog_field_handleMsg=执行备注 +joblog_field_executorAddress=执行器地址 +joblog_clean=清理 +joblog_clean_log=日志清理 +joblog_clean_type=清理方式 +joblog_clean_type_1=清理一个月之前日志数据 +joblog_clean_type_2=清理三个月之前日志数据 +joblog_clean_type_3=清理六个月之前日志数据 +joblog_clean_type_4=清理一年之前日志数据 +joblog_clean_type_5=清理一千条以前日志数据 +joblog_clean_type_6=清理一万条以前日志数据 +joblog_clean_type_7=清理三万条以前日志数据 +joblog_clean_type_8=清理十万条以前日志数据 +joblog_clean_type_9=清理所有日志数据 +joblog_clean_type_unvalid=清理类型参数异常 +joblog_handleCode_200=成功 +joblog_handleCode_500=失败 +joblog_handleCode_502=失败(超时) +joblog_kill_log=终止任务 +joblog_kill_log_limit=调度失败,无法终止日志 +joblog_kill_log_byman=人为操作,主动终止 +joblog_lost_fail=任务结果丢失,标记失败 +joblog_rolling_log=执行日志 +joblog_rolling_log_refresh=刷新 +joblog_rolling_log_triggerfail=任务发起调度失败,无法查看执行日志 +joblog_rolling_log_failoften=终止请求Rolling日志,请求失败次数超上限,可刷新页面重新加载日志 +joblog_logid_unvalid=日志ID非法 + +## job group +jobgroup_name=执行器管理 +jobgroup_list=执行器列表 +jobgroup_add=新增执行器 +jobgroup_edit=编辑执行器 +jobgroup_del=删除执行器 +jobgroup_field_title=名称 +jobgroup_field_addressType=注册方式 +jobgroup_field_addressType_0=自动注册 +jobgroup_field_addressType_1=手动录入 +jobgroup_field_addressType_limit=手动录入注册方式,机器地址不可为空 +jobgroup_field_registryList=机器地址 +jobgroup_field_registryList_unvalid=机器地址格式非法 +jobgroup_field_registryList_placeholder=请输入执行器地址列表,多地址逗号分隔 +jobgroup_field_appname_limit=限制以小写字母开头,由小写字母、数字和中划线组成 +jobgroup_field_appname_length=AppName长度限制为4~64 +jobgroup_field_title_length=名称长度限制为4~12 +jobgroup_field_order_digits=请输入整数 +jobgroup_field_orderrange=取值范围为1~1000 +jobgroup_del_limit_0=拒绝删除,该执行器使用中 +jobgroup_del_limit_1=拒绝删除, 系统至少保留一个执行器 +jobgroup_empty=不存在有效执行器,请联系管理员 + +## job conf +jobconf_block_SERIAL_EXECUTION=单机串行 +jobconf_block_DISCARD_LATER=丢弃后续调度 +jobconf_block_COVER_EARLY=覆盖之前调度 +jobconf_route_first=第一个 +jobconf_route_last=最后一个 +jobconf_route_round=轮询 +jobconf_route_random=随机 +jobconf_route_consistenthash=一致性HASH +jobconf_route_lfu=最不经常使用 +jobconf_route_lru=最近最久未使用 +jobconf_route_failover=故障转移 +jobconf_route_busyover=忙碌转移 +jobconf_route_shard=分片广播 +jobconf_idleBeat=空闲检测 +jobconf_beat=心跳检测 +jobconf_monitor=任务调度中心监控报警 +jobconf_monitor_detail=监控告警明细 +jobconf_monitor_alarm_title=告警类型 +jobconf_monitor_alarm_type=调度失败 +jobconf_monitor_alarm_content=告警内容 +jobconf_trigger_admin_adress=调度机器 +jobconf_trigger_exe_regtype=执行器-注册方式 +jobconf_trigger_exe_regaddress=执行器-地址列表 +jobconf_trigger_address_empty=调度失败:执行器地址为空 +jobconf_trigger_run=触发调度 +jobconf_trigger_child_run=触发子任务 +jobconf_callback_child_msg1={0}/{1} [任务ID={2}], 触发{3}, 触发备注: {4} <br> +jobconf_callback_child_msg2={0}/{1} [任务ID={2}], 触发失败, 触发备注: 任务ID格式错误 <br> +jobconf_trigger_type=任务触发类型 +jobconf_trigger_type_cron=Cron触发 +jobconf_trigger_type_manual=手动触发 +jobconf_trigger_type_parent=父任务触发 +jobconf_trigger_type_api=API触发 +jobconf_trigger_type_retry=失败重试触发 +jobconf_trigger_type_misfire=调度过期补偿 + +## user +user_manage=用户管理 +user_username=账号 +user_password=密码 +user_role=角色 +user_role_admin=管理员 +user_role_normal=普通用户 +user_permission=权限 +user_add=新增用户 +user_update=更新用户 +user_username_repeat=账号重复 +user_username_valid=限制以小写字母开头,由小写字母、数字组成 +user_password_update_placeholder=请输入新密码,为空则不更新密码 +user_update_loginuser_limit=禁止操作当前登录账号 + +## help +job_help=使用教程 +job_help_document=官方文档 \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties new file mode 100644 index 0000000..35916c0 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties @@ -0,0 +1,276 @@ +admin_name=任務調度中心 +admin_name_full=分布式任務調度平臺XXL-JOB +admin_version=2.3.1 +admin_i18n= + +## system +system_tips=系統提示 +system_ok=確定 +system_close=關閉 +system_save=儲存 +system_cancel=取消 +system_search=搜尋 +system_status=狀態 +system_opt=操作 +system_please_input=請輸入 +system_please_choose=请選擇 +system_success=成功 +system_fail=失敗 +system_add_suc=新增成功 +system_add_fail=新增失敗 +system_update_suc=更新成功 +system_update_fail=更新失敗 +system_all=全部 +system_api_error=API錯誤 +system_show=查看 +system_empty=無 +system_opt_suc=操作成功 +system_opt_fail=操作失敗 +system_opt_edit=編輯 +system_opt_del=刪除 +system_opt_copy=復制 +system_unvalid=非法 +system_not_found=不存在 +system_nav=導航 +system_digits=整數 +system_lengh_limit=長度限制 +system_permission_limit=權限控管 +system_welcome=歡迎 + +## daterangepicker +daterangepicker_ranges_recent_hour=最近一小時 +daterangepicker_ranges_today=今日 +daterangepicker_ranges_yesterday=昨日 +daterangepicker_ranges_this_month=本月 +daterangepicker_ranges_last_month=上個月 +daterangepicker_ranges_recent_week=最近一周 +daterangepicker_ranges_recent_month=最近一月 +daterangepicker_custom_name=自定義 +daterangepicker_custom_starttime=起始時間 +daterangepicker_custom_endtime=結束時間 +daterangepicker_custom_daysofweek=日,一,二,三,四,五,六 +daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing=處理中... +dataTable_sLengthMenu=每頁 _MENU_ 條記錄 +dataTable_sZeroRecords=沒有相符合記錄 +dataTable_sInfo=第 _PAGE_ 頁 ( 總共 _PAGES_ 頁,_TOTAL_ 條記錄 ) +dataTable_sInfoEmpty=無記錄 +dataTable_sInfoFiltered=(由 _MAX_ 項結果過濾) +dataTable_sSearch=搜尋 +dataTable_sEmptyTable=表中資料為空 +dataTable_sLoadingRecords=載入中... +dataTable_sFirst=首頁 +dataTable_sPrevious=上頁 +dataTable_sNext=下頁 +dataTable_sLast=末頁 +dataTable_sSortAscending=: 以升幂排序此列 +dataTable_sSortDescending=: 以降幂排序此列 + +## login +login_btn=登入 +login_remember_me=記住密碼 +login_username_placeholder=請輸入登入帳號 +login_password_placeholder=請輸入登入密碼 +login_username_empty=請輸入登入帳號 +login_username_lt_4=登入帳號不應低於4位數 +login_password_empty=請輸入登入密碼 +login_password_lt_4=登入密碼不應低於4位數 +login_success=登入成功 +login_fail=登入失敗 +login_param_empty=帳號或密碼為空值 +login_param_unvalid=帳號或密碼錯誤 + +## logout +logout_btn=登出 +logout_confirm=確認登出? +logout_success=登出成功 +logout_fail=登出失敗 + +## change pwd +change_pwd=修改密碼 +change_pwd_suc_to_logout=修改密碼成功,即將登出 +change_pwd_field_newpwd=新密碼 + +## dashboard +job_dashboard_name=運行報表 +job_dashboard_job_num=任務數量 +job_dashboard_job_num_tip=調度中心運行的任務數量 +job_dashboard_trigger_num=調度次數 +job_dashboard_trigger_num_tip=調度中心觸發的調度次數 +job_dashboard_jobgroup_num=執行器數量 +job_dashboard_jobgroup_num_tip=調度中心在線的執行器機器數量 +job_dashboard_report=調度報表 +job_dashboard_report_loaddata_fail=調度報表資料加載異常 +job_dashboard_date_report=日期分布圖 +job_dashboard_rate_report=成功比例圖 + +## job info +jobinfo_name=任務管理 +jobinfo_job=任務 +jobinfo_field_add=新增 +jobinfo_field_update=更新任務 +jobinfo_field_id=任務ID +jobinfo_field_jobgroup=執行器 +jobinfo_field_jobdesc=任務描述 +jobinfo_field_gluetype=運行模式 +jobinfo_field_executorparam=任務參數 +jobinfo_field_author=負責人 +jobinfo_field_timeout=任務超時秒數 +jobinfo_field_alarmemail=告警郵件 +jobinfo_field_alarmemail_placeholder=輸入多個告警郵件地址,請以逗號分隔 +jobinfo_field_executorRouteStrategy=路由策略 +jobinfo_field_childJobId=子任務ID +jobinfo_field_childJobId_placeholder=輸入子任務ID,如有多個請以逗號分隔 +jobinfo_field_executorBlockStrategy=阻塞處理策略 +jobinfo_field_executorFailRetryCount=失敗重試次數 +jobinfo_field_executorFailRetryCount_placeholder=失敗重試次數,大於零時生效 +jobinfo_script_location=腳本位置 +jobinfo_shard_index=分片序號 +jobinfo_shard_total=分片總數 +jobinfo_opt_stop=停止 +jobinfo_opt_start=啟動 +jobinfo_opt_log=查詢日誌 +jobinfo_opt_run=執行一次 +jobinfo_opt_run_tips=請輸入本次執行的機器地址,為空則從執行器獲取 +jobinfo_opt_registryinfo=注冊節點 +jobinfo_opt_next_time=下次執行時間 +jobinfo_glue_remark=源碼備註 +jobinfo_glue_remark_limit=源碼備註長度限制為4~100 +jobinfo_glue_rollback=版本回復 +jobinfo_glue_jobid_unvalid=任務ID非法 +jobinfo_glue_gluetype_unvalid=該任務非GLUE模式 +jobinfo_field_executorTimeout_placeholder=任務超時時間,單位秒,大於零時生效 +schedule_type=調度類型 +schedule_type_none=無 +schedule_type_cron=CRON +schedule_type_fix_rate=固定速度 +schedule_type_fix_delay=固定延遲 +schedule_type_none_limit_start=當前調度類型禁止啟動 +misfire_strategy=調度過期策略 +misfire_strategy_do_nothing=忽略 +misfire_strategy_fire_once_now=立即執行壹次 +jobinfo_conf_base=基礎配置 +jobinfo_conf_schedule=調度配置 +jobinfo_conf_job=任務配置 +jobinfo_conf_advanced=高級配置 + +## job log +joblog_name=調度日誌 +joblog_status=狀態 +joblog_status_all=全部 +joblog_status_suc=成功 +joblog_status_fail=失敗 +joblog_status_running=進行中 +joblog_field_triggerTime=調度時間 +joblog_field_triggerCode=調度結果 +joblog_field_triggerMsg=調度備註 +joblog_field_handleTime=執行時間 +joblog_field_handleCode=執行结果 +joblog_field_handleMsg=執行備註 +joblog_field_executorAddress=執行器地址 +joblog_clean=清理 +joblog_clean_log=日誌清理 +joblog_clean_type=清理方式 +joblog_clean_type_1=清理一個月之前日誌資料 +joblog_clean_type_2=清理三個月之前日誌資料 +joblog_clean_type_3=清理六個月之前日誌資料 +joblog_clean_type_4=清理一年之前日誌資料 +joblog_clean_type_5=清理一千條以前日誌資料 +joblog_clean_type_6=清理一萬條以前日誌資料 +joblog_clean_type_7=清理三萬條以前日誌資料 +joblog_clean_type_8=清理十萬條以前日誌資料 +joblog_clean_type_9=清理所有日誌資料 +joblog_clean_type_unvalid=清理類型參数異常 +joblog_handleCode_200=成功 +joblog_handleCode_500=失敗 +joblog_handleCode_502=失敗(超時) +joblog_kill_log=终止任務 +joblog_kill_log_limit=調度失敗,無法终止日誌 +joblog_kill_log_byman=人為操作,主動終止 +joblog_lost_fail=任務結果丟失,標記失敗 +joblog_rolling_log=執行日誌 +joblog_rolling_log_refresh=更新 +joblog_rolling_log_triggerfail=任務發起調度失敗,無法查看執行日誌 +joblog_rolling_log_failoften=終止請求Rolling日誌,請求失敗次數超上限,可刷新頁面重新加載日誌 +joblog_logid_unvalid=日誌ID非法 + +## job group +jobgroup_name=執行器管理 +jobgroup_list=執行器列表 +jobgroup_add=新增執行器 +jobgroup_edit=編輯執行器 +jobgroup_del=刪除執行器 +jobgroup_field_title=名稱 +jobgroup_field_addressType=注冊方式 +jobgroup_field_addressType_0=自動注冊 +jobgroup_field_addressType_1=手動登錄 +jobgroup_field_addressType_limit=手動登錄注冊方式,機器地址不可為空 +jobgroup_field_registryList=機器地址 +jobgroup_field_registryList_unvalid=機器地址格式非法 +jobgroup_field_registryList_placeholder=請輸入執行器地址列表,多個地址請以逗號分隔 +jobgroup_field_appname_limit=限制以小寫字母開頭,由小寫字母、數字和中划線組成 +jobgroup_field_appname_length=AppName長度限制為4~64 +jobgroup_field_title_length=名稱長度限制為4~12 +jobgroup_field_order_digits=請輸入整數 +jobgroup_field_orderrange=取值範圍為1~1000 +jobgroup_del_limit_0=拒絕刪除,該執行器使用中 +jobgroup_del_limit_1=拒絕删除,系统至少保留一個執行器 +jobgroup_empty=不存在有效執行器,請聯絡系統管理員 + +## job conf +jobconf_block_SERIAL_EXECUTION=單機串行 +jobconf_block_DISCARD_LATER=丢棄后續調度 +jobconf_block_COVER_EARLY=覆蓋之前調度 +jobconf_route_first=第一個 +jobconf_route_last=最後一個 +jobconf_route_round=輪詢 +jobconf_route_random=隨機 +jobconf_route_consistenthash=一致性HASH +jobconf_route_lfu=最不經常使用 +jobconf_route_lru=最近最久未使用 +jobconf_route_failover=故障轉移 +jobconf_route_busyover=忙碌轉移 +jobconf_route_shard=分片廣播 +jobconf_idleBeat=空閒檢測 +jobconf_beat=心跳檢測 +jobconf_monitor=任務調度中心監控告警 +jobconf_monitor_detail=監控告警明细 +jobconf_monitor_alarm_title=告警類型 +jobconf_monitor_alarm_type=調度失敗 +jobconf_monitor_alarm_content=告警内容 +jobconf_trigger_admin_adress=調度機器 +jobconf_trigger_exe_regtype=執行器-注冊方式 +jobconf_trigger_exe_regaddress=執行器-地址列表 +jobconf_trigger_address_empty=調度失敗:執行器地址為空 +jobconf_trigger_run=觸發調度 +jobconf_trigger_child_run=觸發子任務 +jobconf_callback_child_msg1={0}/{1} [任務ID={2}], 觸發{3}, 觸發備註: {4} <br> +jobconf_callback_child_msg2={0}/{1} [任務ID={2}], 觸發失败, 觸發備註: 任務ID格式錯誤 <br> +jobconf_trigger_type=任務觸發類型 +jobconf_trigger_type_cron=Cron觸發 +jobconf_trigger_type_manual=手動觸發 +jobconf_trigger_type_parent=父任務觸發 +jobconf_trigger_type_api=API觸發 +jobconf_trigger_type_retry=失敗重試觸發 +jobconf_trigger_type_misfire=調度過期補償 + +## user +user_manage=用户管理 +user_username=帳號 +user_password=密碼 +user_role=角色 +user_role_admin=管理員 +user_role_normal=普通用戶 +user_permission=權限 +user_add=新增用戶 +user_update=更新用戶 +user_username_repeat=帳號重複 +user_username_valid=限制以小寫字母開頭,由小寫字母、數字組成 +user_password_update_placeholder=請輸入新密碼,為空則不更新密碼 +user_update_loginuser_limit=禁止操作當前登入帳號 + +## help +job_help=使用教程 +job_help_document=官方文件 \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/logback.xml b/xxl-job/xxl-job-admin/src/main/resources/logback.xml new file mode 100644 index 0000000..d4b08c2 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/logback.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration debug="false" scan="true" scanPeriod="1 seconds"> + + <contextName>logback</contextName> + <property name="log.path" value="/data/applogs/xxl-job/xxl-job-admin.log"/> + + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>${log.path}</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern> + </rollingPolicy> + <encoder> + <pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n + </pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="console"/> + <appender-ref ref="file"/> + </root> + +</configuration> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml new file mode 100644 index 0000000..87299f8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobGroupDao"> + + <resultMap id="XxlJobGroup" type="com.xxl.job.admin.core.model.XxlJobGroup" > + <result column="id" property="id" /> + <result column="app_name" property="appname" /> + <result column="title" property="title" /> + <result column="address_type" property="addressType" /> + <result column="address_list" property="addressList" /> + <result column="update_time" property="updateTime" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.app_name, + t.title, + t.address_type, + t.address_list, + t.update_time + </sql> + + <select id="findAll" resultMap="XxlJobGroup"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_group AS t + ORDER BY t.app_name, t.title, t.id ASC + </select> + + <select id="findByAddressType" parameterType="java.lang.Integer" resultMap="XxlJobGroup"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_group AS t + WHERE t.address_type = #{addressType} + ORDER BY t.app_name, t.title, t.id ASC + </select> + + <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobGroup" useGeneratedKeys="true" keyProperty="id" > + INSERT INTO xxl_job_group ( `app_name`, `title`, `address_type`, `address_list`, `update_time`) + values ( #{appname}, #{title}, #{addressType}, #{addressList}, #{updateTime} ); + </insert> + + <update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobGroup" > + UPDATE xxl_job_group + SET `app_name` = #{appname}, + `title` = #{title}, + `address_type` = #{addressType}, + `address_list` = #{addressList}, + `update_time` = #{updateTime} + WHERE id = #{id} + </update> + + <delete id="remove" parameterType="java.lang.Integer" > + DELETE FROM xxl_job_group + WHERE id = #{id} + </delete> + + <select id="load" parameterType="java.lang.Integer" resultMap="XxlJobGroup"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_group AS t + WHERE t.id = #{id} + </select> + + <select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobGroup"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_group AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="appname != null and appname != ''"> + AND t.app_name like CONCAT(CONCAT('%', #{appname}), '%') + </if> + <if test="title != null and title != ''"> + AND t.title like CONCAT(CONCAT('%', #{title}), '%') + </if> + </trim> + ORDER BY t.app_name, t.title, t.id ASC + LIMIT #{offset}, #{pagesize} + </select> + + <select id="pageListCount" parameterType="java.util.HashMap" resultType="int"> + SELECT count(1) + FROM xxl_job_group AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="appname != null and appname != ''"> + AND t.app_name like CONCAT(CONCAT('%', #{appname}), '%') + </if> + <if test="title != null and title != ''"> + AND t.title like CONCAT(CONCAT('%', #{title}), '%') + </if> + </trim> + </select> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml new file mode 100644 index 0000000..7b3c3a3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml @@ -0,0 +1,240 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobInfoDao"> + + <resultMap id="XxlJobInfo" type="com.xxl.job.admin.core.model.XxlJobInfo" > + <result column="id" property="id" /> + + <result column="job_group" property="jobGroup" /> + <result column="job_desc" property="jobDesc" /> + + <result column="add_time" property="addTime" /> + <result column="update_time" property="updateTime" /> + + <result column="author" property="author" /> + <result column="alarm_email" property="alarmEmail" /> + + <result column="schedule_type" property="scheduleType" /> + <result column="schedule_conf" property="scheduleConf" /> + <result column="misfire_strategy" property="misfireStrategy" /> + + <result column="executor_route_strategy" property="executorRouteStrategy" /> + <result column="executor_handler" property="executorHandler" /> + <result column="executor_param" property="executorParam" /> + <result column="executor_block_strategy" property="executorBlockStrategy" /> + <result column="executor_timeout" property="executorTimeout" /> + <result column="executor_fail_retry_count" property="executorFailRetryCount" /> + + <result column="glue_type" property="glueType" /> + <result column="glue_source" property="glueSource" /> + <result column="glue_remark" property="glueRemark" /> + <result column="glue_updatetime" property="glueUpdatetime" /> + + <result column="child_jobid" property="childJobId" /> + + <result column="trigger_status" property="triggerStatus" /> + <result column="trigger_last_time" property="triggerLastTime" /> + <result column="trigger_next_time" property="triggerNextTime" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.job_group, + t.job_desc, + t.add_time, + t.update_time, + t.author, + t.alarm_email, + t.schedule_type, + t.schedule_conf, + t.misfire_strategy, + t.executor_route_strategy, + t.executor_handler, + t.executor_param, + t.executor_block_strategy, + t.executor_timeout, + t.executor_fail_retry_count, + t.glue_type, + t.glue_source, + t.glue_remark, + t.glue_updatetime, + t.child_jobid, + t.trigger_status, + t.trigger_last_time, + t.trigger_next_time + </sql> + + <select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobInfo"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_info AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="jobGroup gt 0"> + AND t.job_group = #{jobGroup} + </if> + <if test="triggerStatus gte 0"> + AND t.trigger_status = #{triggerStatus} + </if> + <if test="jobDesc != null and jobDesc != ''"> + AND t.job_desc like CONCAT(CONCAT('%', #{jobDesc}), '%') + </if> + <if test="executorHandler != null and executorHandler != ''"> + AND t.executor_handler like CONCAT(CONCAT('%', #{executorHandler}), '%') + </if> + <if test="author != null and author != ''"> + AND t.author like CONCAT(CONCAT('%', #{author}), '%') + </if> + </trim> + ORDER BY id DESC + LIMIT #{offset}, #{pagesize} + </select> + + <select id="pageListCount" parameterType="java.util.HashMap" resultType="int"> + SELECT count(1) + FROM xxl_job_info AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="jobGroup gt 0"> + AND t.job_group = #{jobGroup} + </if> + <if test="triggerStatus gte 0"> + AND t.trigger_status = #{triggerStatus} + </if> + <if test="jobDesc != null and jobDesc != ''"> + AND t.job_desc like CONCAT(CONCAT('%', #{jobDesc}), '%') + </if> + <if test="executorHandler != null and executorHandler != ''"> + AND t.executor_handler like CONCAT(CONCAT('%', #{executorHandler}), '%') + </if> + <if test="author != null and author != ''"> + AND t.author like CONCAT(CONCAT('%', #{author}), '%') + </if> + </trim> + </select> + + <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobInfo" useGeneratedKeys="true" keyProperty="id" > + INSERT INTO xxl_job_info ( + job_group, + job_desc, + add_time, + update_time, + author, + alarm_email, + schedule_type, + schedule_conf, + misfire_strategy, + executor_route_strategy, + executor_handler, + executor_param, + executor_block_strategy, + executor_timeout, + executor_fail_retry_count, + glue_type, + glue_source, + glue_remark, + glue_updatetime, + child_jobid, + trigger_status, + trigger_last_time, + trigger_next_time + ) VALUES ( + #{jobGroup}, + #{jobDesc}, + #{addTime}, + #{updateTime}, + #{author}, + #{alarmEmail}, + #{scheduleType}, + #{scheduleConf}, + #{misfireStrategy}, + #{executorRouteStrategy}, + #{executorHandler}, + #{executorParam}, + #{executorBlockStrategy}, + #{executorTimeout}, + #{executorFailRetryCount}, + #{glueType}, + #{glueSource}, + #{glueRemark}, + #{glueUpdatetime}, + #{childJobId}, + #{triggerStatus}, + #{triggerLastTime}, + #{triggerNextTime} + ); + <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id"> + SELECT LAST_INSERT_ID() + /*SELECT @@IDENTITY AS id*/ + </selectKey>--> + </insert> + + <select id="loadById" parameterType="java.util.HashMap" resultMap="XxlJobInfo"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_info AS t + WHERE t.id = #{id} + </select> + + <update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobInfo" > + UPDATE xxl_job_info + SET + job_group = #{jobGroup}, + job_desc = #{jobDesc}, + update_time = #{updateTime}, + author = #{author}, + alarm_email = #{alarmEmail}, + schedule_type = #{scheduleType}, + schedule_conf = #{scheduleConf}, + misfire_strategy = #{misfireStrategy}, + executor_route_strategy = #{executorRouteStrategy}, + executor_handler = #{executorHandler}, + executor_param = #{executorParam}, + executor_block_strategy = #{executorBlockStrategy}, + executor_timeout = ${executorTimeout}, + executor_fail_retry_count = ${executorFailRetryCount}, + glue_type = #{glueType}, + glue_source = #{glueSource}, + glue_remark = #{glueRemark}, + glue_updatetime = #{glueUpdatetime}, + child_jobid = #{childJobId}, + trigger_status = #{triggerStatus}, + trigger_last_time = #{triggerLastTime}, + trigger_next_time = #{triggerNextTime} + WHERE id = #{id} + </update> + + <delete id="delete" parameterType="java.util.HashMap"> + DELETE + FROM xxl_job_info + WHERE id = #{id} + </delete> + + <select id="getJobsByGroup" parameterType="java.util.HashMap" resultMap="XxlJobInfo"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_info AS t + WHERE t.job_group = #{jobGroup} + </select> + + <select id="findAllCount" resultType="int"> + SELECT count(1) + FROM xxl_job_info + </select> + + + <select id="scheduleJobQuery" parameterType="java.util.HashMap" resultMap="XxlJobInfo"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_info AS t + WHERE t.trigger_status = 1 + and t.trigger_next_time <![CDATA[ <= ]]> #{maxNextTime} + ORDER BY id ASC + LIMIT #{pagesize} + </select> + + <update id="scheduleUpdate" parameterType="com.xxl.job.admin.core.model.XxlJobInfo" > + UPDATE xxl_job_info + SET + trigger_last_time = #{triggerLastTime}, + trigger_next_time = #{triggerNextTime}, + trigger_status = #{triggerStatus} + WHERE id = #{id} + </update> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml new file mode 100644 index 0000000..699277c --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobLogGlueDao"> + + <resultMap id="XxlJobLogGlue" type="com.xxl.job.admin.core.model.XxlJobLogGlue" > + <result column="id" property="id" /> + <result column="job_id" property="jobId" /> + <result column="glue_type" property="glueType" /> + <result column="glue_source" property="glueSource" /> + <result column="glue_remark" property="glueRemark" /> + <result column="add_time" property="addTime" /> + <result column="update_time" property="updateTime" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.job_id, + t.glue_type, + t.glue_source, + t.glue_remark, + t.add_time, + t.update_time + </sql> + + <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobLogGlue" useGeneratedKeys="true" keyProperty="id" > + INSERT INTO xxl_job_logglue ( + `job_id`, + `glue_type`, + `glue_source`, + `glue_remark`, + `add_time`, + `update_time` + ) VALUES ( + #{jobId}, + #{glueType}, + #{glueSource}, + #{glueRemark}, + #{addTime}, + #{updateTime} + ); + <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id"> + SELECT LAST_INSERT_ID() + </selectKey>--> + </insert> + + <select id="findByJobId" parameterType="java.lang.Integer" resultMap="XxlJobLogGlue"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_logglue AS t + WHERE t.job_id = #{jobId} + ORDER BY id DESC + </select> + + <delete id="removeOld" > + DELETE FROM xxl_job_logglue + WHERE id NOT in( + SELECT id FROM( + SELECT id FROM xxl_job_logglue + WHERE `job_id` = #{jobId} + ORDER BY update_time desc + LIMIT 0, #{limit} + ) t1 + ) AND `job_id` = #{jobId} + </delete> + + <delete id="deleteByJobId" parameterType="java.lang.Integer" > + DELETE FROM xxl_job_logglue + WHERE `job_id` = #{jobId} + </delete> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml new file mode 100644 index 0000000..4155f17 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml @@ -0,0 +1,273 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobLogDao"> + + <resultMap id="XxlJobLog" type="com.xxl.job.admin.core.model.XxlJobLog" > + <result column="id" property="id" /> + + <result column="job_group" property="jobGroup" /> + <result column="job_id" property="jobId" /> + + <result column="executor_address" property="executorAddress" /> + <result column="executor_handler" property="executorHandler" /> + <result column="executor_param" property="executorParam" /> + <result column="executor_sharding_param" property="executorShardingParam" /> + <result column="executor_fail_retry_count" property="executorFailRetryCount" /> + + <result column="trigger_time" property="triggerTime" /> + <result column="trigger_code" property="triggerCode" /> + <result column="trigger_msg" property="triggerMsg" /> + + <result column="handle_time" property="handleTime" /> + <result column="handle_code" property="handleCode" /> + <result column="handle_msg" property="handleMsg" /> + + <result column="alarm_status" property="alarmStatus" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.job_group, + t.job_id, + t.executor_address, + t.executor_handler, + t.executor_param, + t.executor_sharding_param, + t.executor_fail_retry_count, + t.trigger_time, + t.trigger_code, + t.trigger_msg, + t.handle_time, + t.handle_code, + t.handle_msg, + t.alarm_status + </sql> + + <select id="pageList" resultMap="XxlJobLog"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_log AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="jobId==0 and jobGroup gt 0"> + AND t.job_group = #{jobGroup} + </if> + <if test="jobId gt 0"> + AND t.job_id = #{jobId} + </if> + <if test="triggerTimeStart != null"> + AND t.trigger_time <![CDATA[ >= ]]> #{triggerTimeStart} + </if> + <if test="triggerTimeEnd != null"> + AND t.trigger_time <![CDATA[ <= ]]> #{triggerTimeEnd} + </if> + <if test="logStatus == 1" > + AND t.handle_code = 200 + </if> + <if test="logStatus == 2" > + AND ( + t.trigger_code NOT IN (0, 200) OR + t.handle_code NOT IN (0, 200) + ) + </if> + <if test="logStatus == 3" > + AND t.trigger_code = 200 + AND t.handle_code = 0 + </if> + </trim> + ORDER BY t.trigger_time DESC + LIMIT #{offset}, #{pagesize} + </select> + + <select id="pageListCount" resultType="int"> + SELECT count(1) + FROM xxl_job_log AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="jobId==0 and jobGroup gt 0"> + AND t.job_group = #{jobGroup} + </if> + <if test="jobId gt 0"> + AND t.job_id = #{jobId} + </if> + <if test="triggerTimeStart != null"> + AND t.trigger_time <![CDATA[ >= ]]> #{triggerTimeStart} + </if> + <if test="triggerTimeEnd != null"> + AND t.trigger_time <![CDATA[ <= ]]> #{triggerTimeEnd} + </if> + <if test="logStatus == 1" > + AND t.handle_code = 200 + </if> + <if test="logStatus == 2" > + AND ( + t.trigger_code NOT IN (0, 200) OR + t.handle_code NOT IN (0, 200) + ) + </if> + <if test="logStatus == 3" > + AND t.trigger_code = 200 + AND t.handle_code = 0 + </if> + </trim> + </select> + + <select id="load" parameterType="java.lang.Long" resultMap="XxlJobLog"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_log AS t + WHERE t.id = #{id} + </select> + + + <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobLog" useGeneratedKeys="true" keyProperty="id" > + INSERT INTO xxl_job_log ( + `job_group`, + `job_id`, + `trigger_time`, + `trigger_code`, + `handle_code` + ) VALUES ( + #{jobGroup}, + #{jobId}, + #{triggerTime}, + #{triggerCode}, + #{handleCode} + ); + <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id"> + SELECT LAST_INSERT_ID() + </selectKey>--> + </insert> + + <update id="updateTriggerInfo" > + UPDATE xxl_job_log + SET + `trigger_time`= #{triggerTime}, + `trigger_code`= #{triggerCode}, + `trigger_msg`= #{triggerMsg}, + `executor_address`= #{executorAddress}, + `executor_handler`=#{executorHandler}, + `executor_param`= #{executorParam}, + `executor_sharding_param`= #{executorShardingParam}, + `executor_fail_retry_count`= #{executorFailRetryCount} + WHERE `id`= #{id} + </update> + + <update id="updateHandleInfo"> + UPDATE xxl_job_log + SET + `handle_time`= #{handleTime}, + `handle_code`= #{handleCode}, + `handle_msg`= #{handleMsg} + WHERE `id`= #{id} + </update> + + <delete id="delete" > + delete from xxl_job_log + WHERE job_id = #{jobId} + </delete> + + <!--<select id="triggerCountByDay" resultType="java.util.Map" > + SELECT + DATE_FORMAT(trigger_time,'%Y-%m-%d') triggerDay, + COUNT(handle_code) triggerDayCount, + SUM(CASE WHEN (trigger_code in (0, 200) and handle_code = 0) then 1 else 0 end) as triggerDayCountRunning, + SUM(CASE WHEN handle_code = 200 then 1 else 0 end) as triggerDayCountSuc + FROM xxl_job_log + WHERE trigger_time BETWEEN #{from} and #{to} + GROUP BY triggerDay + ORDER BY triggerDay + </select>--> + + <select id="findLogReport" resultType="java.util.Map" > + SELECT + COUNT(handle_code) triggerDayCount, + SUM(CASE WHEN (trigger_code in (0, 200) and handle_code = 0) then 1 else 0 end) as triggerDayCountRunning, + SUM(CASE WHEN handle_code = 200 then 1 else 0 end) as triggerDayCountSuc + FROM xxl_job_log + WHERE trigger_time BETWEEN #{from} and #{to} + </select> + + <select id="findClearLogIds" resultType="long" > + SELECT id FROM xxl_job_log + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="jobGroup gt 0"> + AND job_group = #{jobGroup} + </if> + <if test="jobId gt 0"> + AND job_id = #{jobId} + </if> + <if test="clearBeforeTime != null"> + AND trigger_time <![CDATA[ <= ]]> #{clearBeforeTime} + </if> + <if test="clearBeforeNum gt 0"> + AND id NOT in( + SELECT id FROM( + SELECT id FROM xxl_job_log AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="jobGroup gt 0"> + AND t.job_group = #{jobGroup} + </if> + <if test="jobId gt 0"> + AND t.job_id = #{jobId} + </if> + </trim> + ORDER BY t.trigger_time desc + LIMIT 0, #{clearBeforeNum} + ) t1 + ) + </if> + </trim> + order by id asc + LIMIT #{pagesize} + </select> + + <delete id="clearLog" > + delete from xxl_job_log + WHERE id in + <foreach collection="logIds" item="item" open="(" close=")" separator="," > + #{item} + </foreach> + </delete> + + <select id="findFailJobLogIds" resultType="long" > + SELECT id FROM `xxl_job_log` + WHERE !( + (trigger_code in (0, 200) and handle_code = 0) + OR + (handle_code = 200) + ) + AND `alarm_status` = 0 + ORDER BY id ASC + LIMIT #{pagesize} + </select> + + <update id="updateAlarmStatus" > + UPDATE xxl_job_log + SET + `alarm_status` = #{newAlarmStatus} + WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus} + </update> + + <select id="findLostJobIds" resultType="long" > + SELECT + t.id + FROM + xxl_job_log t + LEFT JOIN xxl_job_registry t2 ON t.executor_address = t2.registry_value + WHERE + t.trigger_code = 200 + AND t.handle_code = 0 + AND t.trigger_time <![CDATA[ <= ]]> #{losedTime} + AND t2.id IS NULL; + </select> + <!-- + SELECT t.id + FROM xxl_job_log AS t + WHERE t.trigger_code = 200 + and t.handle_code = 0 + and t.trigger_time <![CDATA[ <= ]]> #{losedTime} + and t.executor_address not in ( + SELECT t2.registry_value + FROM xxl_job_registry AS t2 + ) + --> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml new file mode 100644 index 0000000..579d5f3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobLogReportDao"> + + <resultMap id="XxlJobLogReport" type="com.xxl.job.admin.core.model.XxlJobLogReport" > + <result column="id" property="id" /> + <result column="trigger_day" property="triggerDay" /> + <result column="running_count" property="runningCount" /> + <result column="suc_count" property="sucCount" /> + <result column="fail_count" property="failCount" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.trigger_day, + t.running_count, + t.suc_count, + t.fail_count + </sql> + + <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobLogReport" useGeneratedKeys="true" keyProperty="id" > + INSERT INTO xxl_job_log_report ( + `trigger_day`, + `running_count`, + `suc_count`, + `fail_count` + ) VALUES ( + #{triggerDay}, + #{runningCount}, + #{sucCount}, + #{failCount} + ); + <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id"> + SELECT LAST_INSERT_ID() + </selectKey>--> + </insert> + + <update id="update" > + UPDATE xxl_job_log_report + SET `running_count` = #{runningCount}, + `suc_count` = #{sucCount}, + `fail_count` = #{failCount} + WHERE `trigger_day` = #{triggerDay} + </update> + + <select id="queryLogReport" resultMap="XxlJobLogReport"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_log_report AS t + WHERE t.trigger_day between #{triggerDayFrom} and #{triggerDayTo} + ORDER BY t.trigger_day ASC + </select> + + <select id="queryLogReportTotal" resultMap="XxlJobLogReport"> + SELECT + SUM(running_count) running_count, + SUM(suc_count) suc_count, + SUM(fail_count) fail_count + FROM xxl_job_log_report AS t + </select> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml new file mode 100644 index 0000000..4cae667 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobRegistryDao"> + + <resultMap id="XxlJobRegistry" type="com.xxl.job.admin.core.model.XxlJobRegistry" > + <result column="id" property="id" /> + <result column="registry_group" property="registryGroup" /> + <result column="registry_key" property="registryKey" /> + <result column="registry_value" property="registryValue" /> + <result column="update_time" property="updateTime" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.registry_group, + t.registry_key, + t.registry_value, + t.update_time + </sql> + + <select id="findDead" parameterType="java.util.HashMap" resultType="java.lang.Integer" > + SELECT t.id + FROM xxl_job_registry AS t + WHERE t.update_time <![CDATA[ < ]]> DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND) + </select> + + <delete id="removeDead" parameterType="java.lang.Integer" > + DELETE FROM xxl_job_registry + WHERE id in + <foreach collection="ids" item="item" open="(" close=")" separator="," > + #{item} + </foreach> + </delete> + + <select id="findAll" parameterType="java.util.HashMap" resultMap="XxlJobRegistry"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_registry AS t + WHERE t.update_time <![CDATA[ > ]]> DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND) + </select> + + <update id="registryUpdate" > + UPDATE xxl_job_registry + SET `update_time` = #{updateTime} + WHERE `registry_group` = #{registryGroup} + AND `registry_key` = #{registryKey} + AND `registry_value` = #{registryValue} + </update> + + <insert id="registrySave" > + INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`) + VALUES( #{registryGroup} , #{registryKey} , #{registryValue}, #{updateTime}) + </insert> + + <delete id="registryDelete" > + DELETE FROM xxl_job_registry + WHERE registry_group = #{registryGroup} + AND registry_key = #{registryKey} + AND registry_value = #{registryValue} + </delete> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml new file mode 100644 index 0000000..9e09b4a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.xxl.job.admin.dao.XxlJobUserDao"> + + <resultMap id="XxlJobUser" type="com.xxl.job.admin.core.model.XxlJobUser" > + <result column="id" property="id" /> + <result column="username" property="username" /> + <result column="password" property="password" /> + <result column="role" property="role" /> + <result column="permission" property="permission" /> + </resultMap> + + <sql id="Base_Column_List"> + t.id, + t.username, + t.password, + t.role, + t.permission + </sql> + + <select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobUser"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_user AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="username != null and username != ''"> + AND t.username like CONCAT(CONCAT('%', #{username}), '%') + </if> + <if test="role gt -1"> + AND t.role = #{role} + </if> + </trim> + ORDER BY username ASC + LIMIT #{offset}, #{pagesize} + </select> + + <select id="pageListCount" parameterType="java.util.HashMap" resultType="int"> + SELECT count(1) + FROM xxl_job_user AS t + <trim prefix="WHERE" prefixOverrides="AND | OR" > + <if test="username != null and username != ''"> + AND t.username like CONCAT(CONCAT('%', #{username}), '%') + </if> + <if test="role gt -1"> + AND t.role = #{role} + </if> + </trim> + </select> + + <select id="loadByUserName" parameterType="java.util.HashMap" resultMap="XxlJobUser"> + SELECT <include refid="Base_Column_List" /> + FROM xxl_job_user AS t + WHERE t.username = #{username} + </select> + + <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobUser" useGeneratedKeys="true" keyProperty="id" > + INSERT INTO xxl_job_user ( + username, + password, + role, + permission + ) VALUES ( + #{username}, + #{password}, + #{role}, + #{permission} + ); + </insert> + + <update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobUser" > + UPDATE xxl_job_user + SET + <if test="password != null and password != ''"> + password = #{password}, + </if> + role = #{role}, + permission = #{permission} + WHERE id = #{id} + </update> + + <delete id="delete" parameterType="java.util.HashMap"> + DELETE + FROM xxl_job_user + WHERE id = #{id} + </delete> + +</mapper> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js new file mode 100644 index 0000000..d2720ed --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js @@ -0,0 +1,10 @@ +/*! iCheck v1.0.1 by Damir Sultanov, http://git.io/arlzeA, MIT Licensed */ +(function(h){function F(a,b,d){var c=a[0],e=/er/.test(d)?m:/bl/.test(d)?s:l,f=d==H?{checked:c[l],disabled:c[s],indeterminate:"true"==a.attr(m)||"false"==a.attr(w)}:c[e];if(/^(ch|di|in)/.test(d)&&!f)D(a,e);else if(/^(un|en|de)/.test(d)&&f)t(a,e);else if(d==H)for(e in f)f[e]?D(a,e,!0):t(a,e,!0);else if(!b||"toggle"==d){if(!b)a[p]("ifClicked");f?c[n]!==u&&t(a,e):D(a,e)}}function D(a,b,d){var c=a[0],e=a.parent(),f=b==l,A=b==m,B=b==s,K=A?w:f?E:"enabled",p=k(a,K+x(c[n])),N=k(a,b+x(c[n]));if(!0!==c[b]){if(!d&& +b==l&&c[n]==u&&c.name){var C=a.closest("form"),r='input[name="'+c.name+'"]',r=C.length?C.find(r):h(r);r.each(function(){this!==c&&h(this).data(q)&&t(h(this),b)})}A?(c[b]=!0,c[l]&&t(a,l,"force")):(d||(c[b]=!0),f&&c[m]&&t(a,m,!1));L(a,f,b,d)}c[s]&&k(a,y,!0)&&e.find("."+I).css(y,"default");e[v](N||k(a,b)||"");B?e.attr("aria-disabled","true"):e.attr("aria-checked",A?"mixed":"true");e[z](p||k(a,K)||"")}function t(a,b,d){var c=a[0],e=a.parent(),f=b==l,h=b==m,q=b==s,p=h?w:f?E:"enabled",t=k(a,p+x(c[n])), +u=k(a,b+x(c[n]));if(!1!==c[b]){if(h||!d||"force"==d)c[b]=!1;L(a,f,p,d)}!c[s]&&k(a,y,!0)&&e.find("."+I).css(y,"pointer");e[z](u||k(a,b)||"");q?e.attr("aria-disabled","false"):e.attr("aria-checked","false");e[v](t||k(a,p)||"")}function M(a,b){if(a.data(q)){a.parent().html(a.attr("style",a.data(q).s||""));if(b)a[p](b);a.off(".i").unwrap();h(G+'[for="'+a[0].id+'"]').add(a.closest(G)).off(".i")}}function k(a,b,d){if(a.data(q))return a.data(q).o[b+(d?"":"Class")]}function x(a){return a.charAt(0).toUpperCase()+ +a.slice(1)}function L(a,b,d,c){if(!c){if(b)a[p]("ifToggled");a[p]("ifChanged")[p]("if"+x(d))}}var q="iCheck",I=q+"-helper",u="radio",l="checked",E="un"+l,s="disabled",w="determinate",m="in"+w,H="update",n="type",v="addClass",z="removeClass",p="trigger",G="label",y="cursor",J=/ipad|iphone|ipod|android|blackberry|windows phone|opera mini|silk/i.test(navigator.userAgent);h.fn[q]=function(a,b){var d='input[type="checkbox"], input[type="'+u+'"]',c=h(),e=function(a){a.each(function(){var a=h(this);c=a.is(d)? +c.add(a):c.add(a.find(d))})};if(/^(check|uncheck|toggle|indeterminate|determinate|disable|enable|update|destroy)$/i.test(a))return a=a.toLowerCase(),e(this),c.each(function(){var c=h(this);"destroy"==a?M(c,"ifDestroyed"):F(c,!0,a);h.isFunction(b)&&b()});if("object"!=typeof a&&a)return this;var f=h.extend({checkedClass:l,disabledClass:s,indeterminateClass:m,labelHover:!0,aria:!1},a),k=f.handle,B=f.hoverClass||"hover",x=f.focusClass||"focus",w=f.activeClass||"active",y=!!f.labelHover,C=f.labelHoverClass|| +"hover",r=(""+f.increaseArea).replace("%","")|0;if("checkbox"==k||k==u)d='input[type="'+k+'"]';-50>r&&(r=-50);e(this);return c.each(function(){var a=h(this);M(a);var c=this,b=c.id,e=-r+"%",d=100+2*r+"%",d={position:"absolute",top:e,left:e,display:"block",width:d,height:d,margin:0,padding:0,background:"#fff",border:0,opacity:0},e=J?{position:"absolute",visibility:"hidden"}:r?d:{position:"absolute",opacity:0},k="checkbox"==c[n]?f.checkboxClass||"icheckbox":f.radioClass||"i"+u,m=h(G+'[for="'+b+'"]').add(a.closest(G)), +A=!!f.aria,E=q+"-"+Math.random().toString(36).replace("0.",""),g='<div class="'+k+'" '+(A?'role="'+c[n]+'" ':"");m.length&&A&&m.each(function(){g+='aria-labelledby="';this.id?g+=this.id:(this.id=E,g+=E);g+='"'});g=a.wrap(g+"/>")[p]("ifCreated").parent().append(f.insert);d=h('<ins class="'+I+'"/>').css(d).appendTo(g);a.data(q,{o:f,s:a.attr("style")}).css(e);f.inheritClass&&g[v](c.className||"");f.inheritID&&b&&g.attr("id",q+"-"+b);"static"==g.css("position")&&g.css("position","relative");F(a,!0,H); +if(m.length)m.on("click.i mouseover.i mouseout.i touchbegin.i touchend.i",function(b){var d=b[n],e=h(this);if(!c[s]){if("click"==d){if(h(b.target).is("a"))return;F(a,!1,!0)}else y&&(/ut|nd/.test(d)?(g[z](B),e[z](C)):(g[v](B),e[v](C)));if(J)b.stopPropagation();else return!1}});a.on("click.i focus.i blur.i keyup.i keydown.i keypress.i",function(b){var d=b[n];b=b.keyCode;if("click"==d)return!1;if("keydown"==d&&32==b)return c[n]==u&&c[l]||(c[l]?t(a,l):D(a,l)),!1;if("keyup"==d&&c[n]==u)!c[l]&&D(a,l);else if(/us|ur/.test(d))g["blur"== +d?z:v](x)});d.on("click mousedown mouseup mouseover mouseout touchbegin.i touchend.i",function(b){var d=b[n],e=/wn|up/.test(d)?w:B;if(!c[s]){if("click"==d)F(a,!1,!0);else{if(/wn|er|in/.test(d))g[v](e);else g[z](e+" "+w);if(m.length&&y&&e==B)m[/ut|nd/.test(d)?z:v](C)}if(J)b.stopPropagation();else return!1}})})}})(window.jQuery||window.Zepto); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css new file mode 100644 index 0000000..95340fe --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css @@ -0,0 +1,62 @@ +/* iCheck plugin Square skin, blue +----------------------------------- */ +.icheckbox_square-blue, +.iradio_square-blue { + display: inline-block; + *display: inline; + vertical-align: middle; + margin: 0; + padding: 0; + width: 22px; + height: 22px; + background: url(blue.png) no-repeat; + border: none; + cursor: pointer; +} + +.icheckbox_square-blue { + background-position: 0 0; +} + .icheckbox_square-blue.hover { + background-position: -24px 0; + } + .icheckbox_square-blue.checked { + background-position: -48px 0; + } + .icheckbox_square-blue.disabled { + background-position: -72px 0; + cursor: default; + } + .icheckbox_square-blue.checked.disabled { + background-position: -96px 0; + } + +.iradio_square-blue { + background-position: -120px 0; +} + .iradio_square-blue.hover { + background-position: -144px 0; + } + .iradio_square-blue.checked { + background-position: -168px 0; + } + .iradio_square-blue.disabled { + background-position: -192px 0; + cursor: default; + } + .iradio_square-blue.checked.disabled { + background-position: -216px 0; + } + +/* Retina support */ +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), + only screen and (-moz-min-device-pixel-ratio: 1.5), + only screen and (-o-min-device-pixel-ratio: 3/2), + only screen and (min-device-pixel-ratio: 1.5) { + .icheckbox_square-blue, + .iradio_square-blue { + background-image: url(blue@2x.png); + -webkit-background-size: 240px 24px; + background-size: 240px 24px; + } +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png new file mode 100644 index 0000000..a3e040f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png new file mode 100644 index 0000000..8fdea12 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico b/xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..49c8a29 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js new file mode 100644 index 0000000..1a3fd24 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js @@ -0,0 +1,156 @@ +$(function(){ + + // logout + $("#logoutBtn").click(function(){ + layer.confirm( I18n.logout_confirm , { + icon: 3, + title: I18n.system_tips , + btn: [ I18n.system_ok, I18n.system_cancel ] + }, function(index){ + layer.close(index); + + $.post(base_url + "/logout", function(data, status) { + if (data.code == "200") { + layer.msg( I18n.logout_success ); + setTimeout(function(){ + window.location.href = base_url + "/"; + }, 500); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.logout_fail), + icon: '2' + }); + } + }); + }); + + }); + + // slideToTop + var slideToTop = $("<div />"); + slideToTop.html('<i class="fa fa-chevron-up"></i>'); + slideToTop.css({ + position: 'fixed', + bottom: '20px', + right: '25px', + width: '40px', + height: '40px', + color: '#eee', + 'font-size': '', + 'line-height': '40px', + 'text-align': 'center', + 'background-color': '#222d32', + cursor: 'pointer', + 'border-radius': '5px', + 'z-index': '99999', + opacity: '.7', + 'display': 'none' + }); + slideToTop.on('mouseenter', function () { + $(this).css('opacity', '1'); + }); + slideToTop.on('mouseout', function () { + $(this).css('opacity', '.7'); + }); + $('.wrapper').append(slideToTop); + $(window).scroll(function () { + if ($(window).scrollTop() >= 150) { + if (!$(slideToTop).is(':visible')) { + $(slideToTop).fadeIn(500); + } + } else { + $(slideToTop).fadeOut(500); + } + }); + $(slideToTop).click(function () { + $("html,body").animate({ // firefox ie not support body, chrome support body. but found that new version chrome not support body too. + scrollTop: 0 + }, 100); + }); + + // left menu status v: js + server + cookie + $('.sidebar-toggle').click(function(){ + var xxljob_adminlte_settings = $.cookie('xxljob_adminlte_settings'); // on=open,off=close + if ('off' == xxljob_adminlte_settings) { + xxljob_adminlte_settings = 'on'; + } else { + xxljob_adminlte_settings = 'off'; + } + $.cookie('xxljob_adminlte_settings', xxljob_adminlte_settings, { expires: 7 }); //$.cookie('the_cookie', '', { expires: -1 }); + }); + + // left menu status v1: js + cookie + /* + var xxljob_adminlte_settings = $.cookie('xxljob_adminlte_settings'); + if (xxljob_adminlte_settings == 'off') { + $('body').addClass('sidebar-collapse'); + } + */ + + + // update pwd + $('#updatePwd').on('click', function(){ + $('#updatePwdModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var updatePwdModalValidate = $("#updatePwdModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + password : { + required : true , + rangelength:[4,50] + } + }, + messages : { + password : { + required : '请输入密码' , + rangelength : "密码长度限制为4~50" + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + $.post(base_url + "/user/updatePwd", $("#updatePwdModal .form").serialize(), function(data, status) { + if (data.code == 200) { + $('#updatePwdModal').modal('hide'); + + layer.msg( I18n.change_pwd_suc_to_logout ); + setTimeout(function(){ + $.post(base_url + "/logout", function(data, status) { + if (data.code == 200) { + window.location.href = base_url + "/"; + } else { + layer.open({ + icon: '2', + content: (data.msg|| I18n.logout_fail) + }); + } + }); + }, 500); + } else { + layer.open({ + icon: '2', + content: (data.msg|| I18n.change_pwd + I18n.system_fail ) + }); + } + }); + } + }); + $("#updatePwdModal").on('hide.bs.modal', function () { + $("#updatePwdModal .form")[0].reset(); + updatePwdModalValidate.resetForm(); + $("#updatePwdModal .form .form-group").removeClass("has-error"); + }); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/index.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/index.js new file mode 100644 index 0000000..09111c5 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/index.js @@ -0,0 +1,207 @@ +/** + * Created by xuxueli on 17/4/24. + */ +$(function () { + + // filter Time + var rangesConf = {}; + rangesConf[I18n.daterangepicker_ranges_today] = [moment().startOf('day'), moment().endOf('day')]; + rangesConf[I18n.daterangepicker_ranges_yesterday] = [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')]; + rangesConf[I18n.daterangepicker_ranges_this_month] = [moment().startOf('month'), moment().endOf('month')]; + rangesConf[I18n.daterangepicker_ranges_last_month] = [moment().subtract(1, 'months').startOf('month'), moment().subtract(1, 'months').endOf('month')]; + rangesConf[I18n.daterangepicker_ranges_recent_week] = [moment().subtract(1, 'weeks').startOf('day'), moment().endOf('day')]; + rangesConf[I18n.daterangepicker_ranges_recent_month] = [moment().subtract(1, 'months').startOf('day'), moment().endOf('day')]; + + $('#filterTime').daterangepicker({ + autoApply:false, + singleDatePicker:false, + showDropdowns:false, // 是否显示年月选择条件 + timePicker: true, // 是否显示小时和分钟选择条件 + timePickerIncrement: 10, // 时间的增量,单位为分钟 + timePicker24Hour : true, + opens : 'left', //日期选择框的弹出位置 + ranges: rangesConf, + locale : { + format: 'YYYY-MM-DD HH:mm:ss', + separator : ' - ', + customRangeLabel : I18n.daterangepicker_custom_name , + applyLabel : I18n.system_ok , + cancelLabel : I18n.system_cancel , + fromLabel : I18n.daterangepicker_custom_starttime , + toLabel : I18n.daterangepicker_custom_endtime , + daysOfWeek : I18n.daterangepicker_custom_daysofweek.split(',') , // '日', '一', '二', '三', '四', '五', '六' + monthNames : I18n.daterangepicker_custom_monthnames.split(',') , // '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月' + firstDay : 1 + }, + startDate: rangesConf[I18n.daterangepicker_ranges_recent_week][0] , + endDate: rangesConf[I18n.daterangepicker_ranges_recent_week][1] + }, function (start, end, label) { + freshChartDate(start, end); + }); + freshChartDate(rangesConf[I18n.daterangepicker_ranges_recent_week][0], rangesConf[I18n.daterangepicker_ranges_recent_week][1]); + + /** + * fresh Chart Date + * + * @param startDate + * @param endDate + */ + function freshChartDate(startDate, endDate) { + $.ajax({ + type : 'POST', + url : base_url + '/chartInfo', + data : { + 'startDate':startDate.format('YYYY-MM-DD HH:mm:ss'), + 'endDate':endDate.format('YYYY-MM-DD HH:mm:ss') + }, + dataType : "json", + success : function(data){ + if (data.code == 200) { + lineChartInit(data) + pieChartInit(data); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.job_dashboard_report_loaddata_fail ), + icon: '2' + }); + } + } + }); + } + + /** + * line Chart Init + */ + function lineChartInit(data) { + var option = { + title: { + text: I18n.job_dashboard_date_report + }, + tooltip : { + trigger: 'axis', + axisPointer: { + type: 'cross', + label: { + backgroundColor: '#6a7985' + } + } + }, + legend: { + data:[I18n.joblog_status_suc, I18n.joblog_status_fail, I18n.joblog_status_running] + }, + toolbox: { + feature: { + /*saveAsImage: {}*/ + } + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis : [ + { + type : 'category', + boundaryGap : false, + data : data.content.triggerDayList + } + ], + yAxis : [ + { + type : 'value' + } + ], + series : [ + { + name:I18n.joblog_status_suc, + type:'line', + stack: 'Total', + areaStyle: {normal: {}}, + data: data.content.triggerDayCountSucList + }, + { + name:I18n.joblog_status_fail, + type:'line', + stack: 'Total', + label: { + normal: { + show: true, + position: 'top' + } + }, + areaStyle: {normal: {}}, + data: data.content.triggerDayCountFailList + }, + { + name:I18n.joblog_status_running, + type:'line', + stack: 'Total', + areaStyle: {normal: {}}, + data: data.content.triggerDayCountRunningList + } + ], + color:['#00A65A', '#c23632', '#F39C12'] + }; + + var lineChart = echarts.init(document.getElementById('lineChart')); + lineChart.setOption(option); + } + + /** + * pie Chart Init + */ + function pieChartInit(data) { + var option = { + title : { + text: I18n.job_dashboard_rate_report , + /*subtext: 'subtext',*/ + x:'center' + }, + tooltip : { + trigger: 'item', + formatter: "{b} : {c} ({d}%)" + }, + legend: { + orient: 'vertical', + left: 'left', + data: [I18n.joblog_status_suc, I18n.joblog_status_fail, I18n.joblog_status_running ] + }, + series : [ + { + //name: '分布比例', + type: 'pie', + radius : '55%', + center: ['50%', '60%'], + data:[ + { + name:I18n.joblog_status_suc, + value:data.content.triggerCountSucTotal + }, + { + name:I18n.joblog_status_fail, + value:data.content.triggerCountFailTotal + }, + { + name:I18n.joblog_status_running, + value:data.content.triggerCountRunningTotal + } + ], + itemStyle: { + emphasis: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ], + color:['#00A65A', '#c23632', '#F39C12'] + }; + var pieChart = echarts.init(document.getElementById('pieChart')); + pieChart.setOption(option); + } + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js new file mode 100644 index 0000000..668d634 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js @@ -0,0 +1,97 @@ +$(function() { + + // init code editor + var codeEditor; + function initIde(glueSource) { + if (codeEditor == null) { + codeEditor = CodeMirror(document.getElementById("ideWindow"), { + mode : ideMode, + lineNumbers : true, + matchBrackets : true, + value: glueSource + }); + } else { + codeEditor.setValue(glueSource); + } + } + + initIde($("#version_now").val()); + + // code change + $(".source_version").click(function(){ + var sourceId = $(this).attr('version'); + var temp = $( "#" + sourceId ).val(); + + //codeEditor.setValue(''); + initIde(temp); + }); + + // code source save + $("#save").click(function() { + $('#saveModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + + $("#saveModal .ok").click(function() { + + var glueSource = codeEditor.getValue(); + var glueRemark = $("#glueRemark").val(); + + if (!glueRemark) { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok], + content: I18n.system_please_input + I18n.jobinfo_glue_remark , + icon: '2' + }); + return; + } + if (glueRemark.length <4 || glueRemark.length > 100) { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: I18n.jobinfo_glue_remark_limit , + icon: '2' + }); + return; + } + + $.ajax({ + type : 'POST', + url : base_url + '/jobcode/save', + data : { + 'id' : id, + 'glueSource' : glueSource, + 'glueRemark' : glueRemark + }, + dataType : "json", + success : function(data){ + if (data.code == 200) { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (I18n.system_save + I18n.system_success) , + icon: '1', + end: function(layero, index){ + //$(window).unbind('beforeunload'); + window.location.reload(); + } + }); + } else { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (data.msg || (I18n.system_save + I18n.system_fail) ), + icon: '2' + }); + } + } + }); + + }); + + // before upload + /*$(window).bind('beforeunload',function(){ + return 'Glue尚未保存,确定离开Glue编辑器?'; + });*/ + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js new file mode 100644 index 0000000..5eb6559 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js @@ -0,0 +1,359 @@ +$(function() { + + // init date tables + var jobGroupTable = $("#jobgroup_list").dataTable({ + "deferRender": true, + "processing" : true, + "serverSide": true, + "ajax": { + url: base_url + "/jobgroup/pageList", + type:"post", + data : function ( d ) { + var obj = {}; + obj.appname = $('#appname').val(); + obj.title = $('#title').val(); + obj.start = d.start; + obj.length = d.length; + return obj; + } + }, + "searching": false, + "ordering": false, + //"scrollX": true, // scroll x,close self-adaption + "columns": [ + { + "data": 'id', + "visible" : false + }, + { + "data": 'appname', + "visible" : true, + "width":'30%' + }, + { + "data": 'title', + "visible" : true, + "width":'30%' + }, + { + "data": 'addressType', + "width":'10%', + "visible" : true, + "render": function ( data, type, row ) { + if (row.addressType == 0) { + return I18n.jobgroup_field_addressType_0; + } else { + return I18n.jobgroup_field_addressType_1; + } + } + }, + { + "data": 'registryList', + "width":'15%', + "visible" : true, + "render": function ( data, type, row ) { + return row.registryList + ?'<a class="show_registryList" href="javascript:;" _id="'+ row.id +'" >' + + I18n.system_show +' ( ' + row.registryList.length+ ' )</a>' + :I18n.system_empty; + } + }, + { + "data": I18n.system_opt , + "width":'15%', + "render": function ( data, type, row ) { + return function(){ + // data + tableData['key'+row.id] = row; + + // opt + var html = '<div class="btn-group">\n' + + ' <button type="button" class="btn btn-primary btn-sm">'+ I18n.system_opt +'</button>\n' + + ' <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">\n' + + ' <span class="caret"></span>\n' + + ' <span class="sr-only">Toggle Dropdown</span>\n' + + ' </button>\n' + + ' <ul class="dropdown-menu" role="menu" _id="'+ row.id +'" >\n' + + ' <li><a href="javascript:void(0);" class="opt_edit" >'+ I18n.system_opt_edit +'</a></li>\n' + + ' <li><a href="javascript:void(0);" class="opt_del" >'+ I18n.system_opt_del +'</a></li>\n' + + ' </ul>\n' + + ' </div>'; + + return html; + }; + } + } + ], + "language" : { + "sProcessing" : I18n.dataTable_sProcessing , + "sLengthMenu" : I18n.dataTable_sLengthMenu , + "sZeroRecords" : I18n.dataTable_sZeroRecords , + "sInfo" : I18n.dataTable_sInfo , + "sInfoEmpty" : I18n.dataTable_sInfoEmpty , + "sInfoFiltered" : I18n.dataTable_sInfoFiltered , + "sInfoPostFix" : "", + "sSearch" : I18n.dataTable_sSearch , + "sUrl" : "", + "sEmptyTable" : I18n.dataTable_sEmptyTable , + "sLoadingRecords" : I18n.dataTable_sLoadingRecords , + "sInfoThousands" : ",", + "oPaginate" : { + "sFirst" : I18n.dataTable_sFirst , + "sPrevious" : I18n.dataTable_sPrevious , + "sNext" : I18n.dataTable_sNext , + "sLast" : I18n.dataTable_sLast + }, + "oAria" : { + "sSortAscending" : I18n.dataTable_sSortAscending , + "sSortDescending" : I18n.dataTable_sSortDescending + } + } + }); + + // table data + var tableData = {}; + + // search btn + $('#searchBtn').on('click', function(){ + jobGroupTable.fnDraw(); + }); + + // job registryinfo + $("#jobgroup_list").on('click', '.show_registryList',function() { + var id = $(this).attr("_id"); + var row = tableData['key'+id]; + + var html = '<div>'; + if (row.registryList) { + for (var index in row.registryList) { + html += (parseInt(index)+1) + '. <span class="badge bg-green" >' + row.registryList[index] + '</span><br>'; + } + } + html += '</div>'; + + layer.open({ + title: I18n.jobinfo_opt_registryinfo , + btn: [ I18n.system_ok ], + content: html + }); + + }); + + + // opt_del + $("#jobgroup_list").on('click', '.opt_del',function() { + var id = $(this).parents('ul').attr("_id"); + + layer.confirm( (I18n.system_ok + I18n.jobgroup_del + '?') , { + icon: 3, + title: I18n.system_tips , + btn: [ I18n.system_ok, I18n.system_cancel ] + }, function(index){ + layer.close(index); + + $.ajax({ + type : 'POST', + url : base_url + '/jobgroup/remove', + data : {"id":id}, + dataType : "json", + success : function(data){ + if (data.code == 200) { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (I18n.jobgroup_del + I18n.system_success), + icon: '1', + end: function(layero, index){ + jobGroupTable.fnDraw(); + } + }); + } else { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (data.msg || (I18n.jobgroup_del + I18n.system_fail)), + icon: '2' + }); + } + }, + }); + }); + }); + + + // jquery.validate “low letters start, limit contants、 letters、numbers and line-through.” + jQuery.validator.addMethod("myValid01", function(value, element) { + var length = value.length; + var valid = /^[a-z][a-zA-Z0-9-]*$/; + return this.optional(element) || valid.test(value); + }, I18n.jobgroup_field_appname_limit ); + + $('.add').on('click', function(){ + $('#addModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var addModalValidate = $("#addModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + appname : { + required : true, + rangelength:[4,64], + myValid01 : true + }, + title : { + required : true, + rangelength:[4, 12] + } + }, + messages : { + appname : { + required : I18n.system_please_input+"AppName", + rangelength: I18n.jobgroup_field_appname_length , + myValid01: I18n.jobgroup_field_appname_limit + }, + title : { + required : I18n.system_please_input + I18n.jobgroup_field_title , + rangelength: I18n.jobgroup_field_title_length + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + $.post(base_url + "/jobgroup/save", $("#addModal .form").serialize(), function(data, status) { + if (data.code == "200") { + $('#addModal').modal('hide'); + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: I18n.system_add_suc , + icon: '1', + end: function(layero, index){ + jobGroupTable.fnDraw(); + } + }); + } else { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_add_fail ), + icon: '2' + }); + } + }); + } + }); + $("#addModal").on('hide.bs.modal', function () { + $("#addModal .form")[0].reset(); + addModalValidate.resetForm(); + $("#addModal .form .form-group").removeClass("has-error"); + }); + + // addressType change + $("#addModal input[name=addressType], #updateModal input[name=addressType]").click(function(){ + var addressType = $(this).val(); + var $addressList = $(this).parents("form").find("textarea[name=addressList]"); + if (addressType == 0) { + $addressList.css("background-color", "#eee"); // 自动注册 + $addressList.attr("readonly","readonly"); + $addressList.val(""); + } else { + $addressList.css("background-color", "white"); + $addressList.removeAttr("readonly"); + } + }); + + // opt_edit + $("#jobgroup_list").on('click', '.opt_edit',function() { + var id = $(this).parents('ul').attr("_id"); + var row = tableData['key'+id]; + + $("#updateModal .form input[name='id']").val( row.id ); + $("#updateModal .form input[name='appname']").val( row.appname ); + $("#updateModal .form input[name='title']").val( row.title ); + + // 注册方式 + $("#updateModal .form input[name='addressType']").removeAttr('checked'); + $("#updateModal .form input[name='addressType'][value='"+ row.addressType +"']").click(); + // 机器地址 + $("#updateModal .form textarea[name='addressList']").val( row.addressList ); + + $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var updateModalValidate = $("#updateModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + appname : { + required : true, + rangelength:[4,64], + myValid01 : true + }, + title : { + required : true, + rangelength:[4, 12] + } + }, + messages : { + appname : { + required : I18n.system_please_input+"AppName", + rangelength: I18n.jobgroup_field_appname_length , + myValid01: I18n.jobgroup_field_appname_limit + }, + title : { + required : I18n.system_please_input + I18n.jobgroup_field_title , + rangelength: I18n.jobgroup_field_title_length + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + $.post(base_url + "/jobgroup/update", $("#updateModal .form").serialize(), function(data, status) { + if (data.code == "200") { + $('#updateModal').modal('hide'); + + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: I18n.system_update_suc , + icon: '1', + end: function(layero, index){ + jobGroupTable.fnDraw(); + } + }); + } else { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_update_fail ), + icon: '2' + }); + } + }); + } + }); + $("#updateModal").on('hide.bs.modal', function () { + $("#updateModal .form")[0].reset(); + addModalValidate.resetForm(); + $("#updateModal .form .form-group").removeClass("has-error"); + }); + + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js new file mode 100644 index 0000000..b479e97 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js @@ -0,0 +1,739 @@ +$(function() { + + // init date tables + var jobTable = $("#job_list").dataTable({ + "deferRender": true, + "processing" : true, + "serverSide": true, + "ajax": { + url: base_url + "/jobinfo/pageList", + type:"post", + data : function ( d ) { + var obj = {}; + obj.jobGroup = $('#jobGroup').val(); + obj.triggerStatus = $('#triggerStatus').val(); + obj.jobDesc = $('#jobDesc').val(); + obj.executorHandler = $('#executorHandler').val(); + obj.author = $('#author').val(); + obj.start = d.start; + obj.length = d.length; + return obj; + } + }, + "searching": false, + "ordering": false, + //"scrollX": true, // scroll x,close self-adaption + "columns": [ + { + "data": 'id', + "bSortable": false, + "visible" : true, + "width":'7%' + }, + { + "data": 'jobGroup', + "visible" : false, + "render": function ( data, type, row ) { + var groupMenu = $("#jobGroup").find("option"); + for ( var index in $("#jobGroup").find("option")) { + if ($(groupMenu[index]).attr('value') == data) { + return $(groupMenu[index]).html(); + } + } + return data; + } + }, + { + "data": 'jobDesc', + "visible" : true, + "width":'25%' + }, + { + "data": 'scheduleType', + "visible" : true, + "width":'13%', + "render": function ( data, type, row ) { + if (row.scheduleConf) { + return row.scheduleType + ':'+ row.scheduleConf; + } else { + return row.scheduleType; + } + } + }, + { + "data": 'glueType', + "width":'25%', + "visible" : true, + "render": function ( data, type, row ) { + var glueTypeTitle = findGlueTypeTitle(row.glueType); + if (row.executorHandler) { + return glueTypeTitle +":" + row.executorHandler; + } else { + return glueTypeTitle; + } + } + }, + { "data": 'executorParam', "visible" : false}, + { + "data": 'addTime', + "visible" : false, + "render": function ( data, type, row ) { + return data?moment(new Date(data)).format("YYYY-MM-DD HH:mm:ss"):""; + } + }, + { + "data": 'updateTime', + "visible" : false, + "render": function ( data, type, row ) { + return data?moment(new Date(data)).format("YYYY-MM-DD HH:mm:ss"):""; + } + }, + { "data": 'author', "visible" : true, "width":'10%'}, + { "data": 'alarmEmail', "visible" : false}, + { + "data": 'triggerStatus', + "width":'10%', + "visible" : true, + "render": function ( data, type, row ) { + // status + if (1 == data) { + return '<small class="label label-success" >RUNNING</small>'; + } else { + return '<small class="label label-default" >STOP</small>'; + } + return data; + } + }, + { + "data": I18n.system_opt , + "width":'10%', + "render": function ( data, type, row ) { + return function(){ + + // status + var start_stop_div = ""; + if (1 == row.triggerStatus ) { + start_stop_div = '<li><a href="javascript:void(0);" class="job_operate" _type="job_pause" >'+ I18n.jobinfo_opt_stop +'</a></li>\n'; + } else { + start_stop_div = '<li><a href="javascript:void(0);" class="job_operate" _type="job_resume" >'+ I18n.jobinfo_opt_start +'</a></li>\n'; + } + + // job_next_time_html + var job_next_time_html = ''; + if (row.scheduleType == 'CRON' || row.scheduleType == 'FIX_RATE') { + job_next_time_html = '<li><a href="javascript:void(0);" class="job_next_time" >' + I18n.jobinfo_opt_next_time + '</a></li>\n'; + } + + // log url + var logHref = base_url +'/joblog?jobId='+ row.id; + + // code url + var codeBtn = ""; + if ('BEAN' != row.glueType) { + var codeUrl = base_url +'/jobcode?jobId='+ row.id; + codeBtn = '<li><a href="'+ codeUrl +'" target="_blank" >GLUE IDE</a></li>\n'; + codeBtn += '<li class="divider"></li>\n'; + } + + // data + tableData['key'+row.id] = row; + + // opt + var html = '<div class="btn-group">\n' + + ' <button type="button" class="btn btn-primary btn-sm">'+ I18n.system_opt +'</button>\n' + + ' <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">\n' + + ' <span class="caret"></span>\n' + + ' <span class="sr-only">Toggle Dropdown</span>\n' + + ' </button>\n' + + ' <ul class="dropdown-menu" role="menu" _id="'+ row.id +'" >\n' + + ' <li><a href="javascript:void(0);" class="job_trigger" >'+ I18n.jobinfo_opt_run +'</a></li>\n' + + ' <li><a href="'+ logHref +'">'+ I18n.jobinfo_opt_log +'</a></li>\n' + + ' <li><a href="javascript:void(0);" class="job_registryinfo" >' + I18n.jobinfo_opt_registryinfo + '</a></li>\n' + + job_next_time_html + + ' <li class="divider"></li>\n' + + codeBtn + + start_stop_div + + ' <li><a href="javascript:void(0);" class="update" >'+ I18n.system_opt_edit +'</a></li>\n' + + ' <li><a href="javascript:void(0);" class="job_operate" _type="job_del" >'+ I18n.system_opt_del +'</a></li>\n' + + ' <li><a href="javascript:void(0);" class="job_copy" >'+ I18n.system_opt_copy +'</a></li>\n' + + ' </ul>\n' + + ' </div>'; + + return html; + }; + } + } + ], + "language" : { + "sProcessing" : I18n.dataTable_sProcessing , + "sLengthMenu" : I18n.dataTable_sLengthMenu , + "sZeroRecords" : I18n.dataTable_sZeroRecords , + "sInfo" : I18n.dataTable_sInfo , + "sInfoEmpty" : I18n.dataTable_sInfoEmpty , + "sInfoFiltered" : I18n.dataTable_sInfoFiltered , + "sInfoPostFix" : "", + "sSearch" : I18n.dataTable_sSearch , + "sUrl" : "", + "sEmptyTable" : I18n.dataTable_sEmptyTable , + "sLoadingRecords" : I18n.dataTable_sLoadingRecords , + "sInfoThousands" : ",", + "oPaginate" : { + "sFirst" : I18n.dataTable_sFirst , + "sPrevious" : I18n.dataTable_sPrevious , + "sNext" : I18n.dataTable_sNext , + "sLast" : I18n.dataTable_sLast + }, + "oAria" : { + "sSortAscending" : I18n.dataTable_sSortAscending , + "sSortDescending" : I18n.dataTable_sSortDescending + } + } + }); + + // table data + var tableData = {}; + + // search btn + $('#searchBtn').on('click', function(){ + jobTable.fnDraw(); + }); + + // jobGroup change + $('#jobGroup').on('change', function(){ + //reload + var jobGroup = $('#jobGroup').val(); + window.location.href = base_url + "/jobinfo?jobGroup=" + jobGroup; + }); + + // job operate + $("#job_list").on('click', '.job_operate',function() { + var typeName; + var url; + var needFresh = false; + + var type = $(this).attr("_type"); + if ("job_pause" == type) { + typeName = I18n.jobinfo_opt_stop ; + url = base_url + "/jobinfo/stop"; + needFresh = true; + } else if ("job_resume" == type) { + typeName = I18n.jobinfo_opt_start ; + url = base_url + "/jobinfo/start"; + needFresh = true; + } else if ("job_del" == type) { + typeName = I18n.system_opt_del ; + url = base_url + "/jobinfo/remove"; + needFresh = true; + } else { + return; + } + + var id = $(this).parents('ul').attr("_id"); + + layer.confirm( I18n.system_ok + typeName + '?', { + icon: 3, + title: I18n.system_tips , + btn: [ I18n.system_ok, I18n.system_cancel ] + }, function(index){ + layer.close(index); + + $.ajax({ + type : 'POST', + url : url, + data : { + "id" : id + }, + dataType : "json", + success : function(data){ + if (data.code == 200) { + layer.msg( typeName + I18n.system_success ); + if (needFresh) { + //window.location.reload(); + jobTable.fnDraw(false); + } + } else { + layer.msg( data.msg || typeName + I18n.system_fail ); + } + } + }); + }); + }); + + // job trigger + $("#job_list").on('click', '.job_trigger',function() { + var id = $(this).parents('ul').attr("_id"); + var row = tableData['key'+id]; + + $("#jobTriggerModal .form input[name='id']").val( row.id ); + $("#jobTriggerModal .form textarea[name='executorParam']").val( row.executorParam ); + + $('#jobTriggerModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + $("#jobTriggerModal .ok").on('click',function() { + $.ajax({ + type : 'POST', + url : base_url + "/jobinfo/trigger", + data : { + "id" : $("#jobTriggerModal .form input[name='id']").val(), + "executorParam" : $("#jobTriggerModal .textarea[name='executorParam']").val(), + "addressList" : $("#jobTriggerModal .textarea[name='addressList']").val() + }, + dataType : "json", + success : function(data){ + if (data.code == 200) { + $('#jobTriggerModal').modal('hide'); + + layer.msg( I18n.jobinfo_opt_run + I18n.system_success ); + } else { + layer.msg( data.msg || I18n.jobinfo_opt_run + I18n.system_fail ); + } + } + }); + }); + $("#jobTriggerModal").on('hide.bs.modal', function () { + $("#jobTriggerModal .form")[0].reset(); + }); + + + // job registryinfo + $("#job_list").on('click', '.job_registryinfo',function() { + var id = $(this).parents('ul').attr("_id"); + var row = tableData['key'+id]; + + var jobGroup = row.jobGroup; + + $.ajax({ + type : 'POST', + url : base_url + "/jobgroup/loadById", + data : { + "id" : jobGroup + }, + dataType : "json", + success : function(data){ + + var html = '<div>'; + if (data.code == 200 && data.content.registryList) { + for (var index in data.content.registryList) { + html += (parseInt(index)+1) + '. <span class="badge bg-green" >' + data.content.registryList[index] + '</span><br>'; + } + } + html += '</div>'; + + layer.open({ + title: I18n.jobinfo_opt_registryinfo , + btn: [ I18n.system_ok ], + content: html + }); + + } + }); + + }); + + // job_next_time + $("#job_list").on('click', '.job_next_time',function() { + var id = $(this).parents('ul').attr("_id"); + var row = tableData['key'+id]; + + $.ajax({ + type : 'POST', + url : base_url + "/jobinfo/nextTriggerTime", + data : { + "scheduleType" : row.scheduleType, + "scheduleConf" : row.scheduleConf + }, + dataType : "json", + success : function(data){ + + if (data.code != 200) { + layer.open({ + title: I18n.jobinfo_opt_next_time , + btn: [ I18n.system_ok ], + content: data.msg + }); + } else { + var html = '<center>'; + if (data.code == 200 && data.content) { + for (var index in data.content) { + html += '<span>' + data.content[index] + '</span><br>'; + } + } + html += '</center>'; + + layer.open({ + title: I18n.jobinfo_opt_next_time , + btn: [ I18n.system_ok ], + content: html + }); + } + + } + }); + + }); + + // add + $(".add").click(function(){ + + // init-cronGen + $("#addModal .form input[name='schedule_conf_CRON']").show().siblings().remove(); + $("#addModal .form input[name='schedule_conf_CRON']").cronGen({}); + + // 》init scheduleType + $("#updateModal .form select[name=scheduleType]").change(); + + // 》init glueType + $("#updateModal .form select[name=glueType]").change(); + + $('#addModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var addModalValidate = $("#addModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + jobDesc : { + required : true, + maxlength: 50 + }, + author : { + required : true + }/*, + executorTimeout : { + digits:true + }, + executorFailRetryCount : { + digits:true + }*/ + }, + messages : { + jobDesc : { + required : I18n.system_please_input + I18n.jobinfo_field_jobdesc + }, + author : { + required : I18n.system_please_input + I18n.jobinfo_field_author + }/*, + executorTimeout : { + digits: I18n.system_please_input + I18n.system_digits + }, + executorFailRetryCount : { + digits: I18n.system_please_input + I18n.system_digits + }*/ + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + + // process executorTimeout+executorFailRetryCount + var executorTimeout = $("#addModal .form input[name='executorTimeout']").val(); + if(!/^\d+$/.test(executorTimeout)) { + executorTimeout = 0; + } + $("#addModal .form input[name='executorTimeout']").val(executorTimeout); + var executorFailRetryCount = $("#addModal .form input[name='executorFailRetryCount']").val(); + if(!/^\d+$/.test(executorFailRetryCount)) { + executorFailRetryCount = 0; + } + $("#addModal .form input[name='executorFailRetryCount']").val(executorFailRetryCount); + + // process schedule_conf + var scheduleType = $("#addModal .form select[name='scheduleType']").val(); + var scheduleConf; + if (scheduleType == 'CRON') { + scheduleConf = $("#addModal .form input[name='cronGen_display']").val(); + } else if (scheduleType == 'FIX_RATE') { + scheduleConf = $("#addModal .form input[name='schedule_conf_FIX_RATE']").val(); + } else if (scheduleType == 'FIX_DELAY') { + scheduleConf = $("#addModal .form input[name='schedule_conf_FIX_DELAY']").val(); + } + $("#addModal .form input[name='scheduleConf']").val( scheduleConf ); + + $.post(base_url + "/jobinfo/add", $("#addModal .form").serialize(), function(data, status) { + if (data.code == "200") { + $('#addModal').modal('hide'); + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: I18n.system_add_suc , + icon: '1', + end: function(layero, index){ + jobTable.fnDraw(); + //window.location.reload(); + } + }); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_add_fail), + icon: '2' + }); + } + }); + } + }); + $("#addModal").on('hide.bs.modal', function () { + addModalValidate.resetForm(); + $("#addModal .form")[0].reset(); + $("#addModal .form .form-group").removeClass("has-error"); + $(".remote_panel").show(); // remote + + $("#addModal .form input[name='executorHandler']").removeAttr("readonly"); + }); + + // scheduleType change + $(".scheduleType").change(function(){ + var scheduleType = $(this).val(); + $(this).parents("form").find(".schedule_conf").hide(); + $(this).parents("form").find(".schedule_conf_" + scheduleType).show(); + + }); + + // glueType change + $(".glueType").change(function(){ + // executorHandler + var $executorHandler = $(this).parents("form").find("input[name='executorHandler']"); + var glueType = $(this).val(); + if ('BEAN' != glueType) { + $executorHandler.val(""); + $executorHandler.attr("readonly","readonly"); + } else { + $executorHandler.removeAttr("readonly"); + } + }); + + $("#addModal .glueType").change(function(){ + // glueSource + var glueType = $(this).val(); + if ('GLUE_GROOVY'==glueType){ + $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_java").val() ); + } else if ('GLUE_SHELL'==glueType){ + $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_shell").val() ); + } else if ('GLUE_PYTHON'==glueType){ + $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_python").val() ); + } else if ('GLUE_PHP'==glueType){ + $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_php").val() ); + } else if ('GLUE_NODEJS'==glueType){ + $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_nodejs").val() ); + } else if ('GLUE_POWERSHELL'==glueType){ + $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_powershell").val() ); + } else { + $("#addModal .form textarea[name='glueSource']").val(""); + } + }); + + // update + $("#job_list").on('click', '.update',function() { + + var id = $(this).parents('ul').attr("_id"); + var row = tableData['key'+id]; + + // fill base + $("#updateModal .form input[name='id']").val( row.id ); + $('#updateModal .form select[name=jobGroup] option[value='+ row.jobGroup +']').prop('selected', true); + $("#updateModal .form input[name='jobDesc']").val( row.jobDesc ); + $("#updateModal .form input[name='author']").val( row.author ); + $("#updateModal .form input[name='alarmEmail']").val( row.alarmEmail ); + + // fill trigger + $('#updateModal .form select[name=scheduleType] option[value='+ row.scheduleType +']').prop('selected', true); + $("#updateModal .form input[name='scheduleConf']").val( row.scheduleConf ); + if (row.scheduleType == 'CRON') { + $("#updateModal .form input[name='schedule_conf_CRON']").val( row.scheduleConf ); + } else if (row.scheduleType == 'FIX_RATE') { + $("#updateModal .form input[name='schedule_conf_FIX_RATE']").val( row.scheduleConf ); + } else if (row.scheduleType == 'FIX_DELAY') { + $("#updateModal .form input[name='schedule_conf_FIX_DELAY']").val( row.scheduleConf ); + } + + // 》init scheduleType + $("#updateModal .form select[name=scheduleType]").change(); + + // fill job + $('#updateModal .form select[name=glueType] option[value='+ row.glueType +']').prop('selected', true); + $("#updateModal .form input[name='executorHandler']").val( row.executorHandler ); + $("#updateModal .form textarea[name='executorParam']").val( row.executorParam ); + + // 》init glueType + $("#updateModal .form select[name=glueType]").change(); + + // 》init-cronGen + $("#updateModal .form input[name='schedule_conf_CRON']").show().siblings().remove(); + $("#updateModal .form input[name='schedule_conf_CRON']").cronGen({}); + + // fill advanced + $('#updateModal .form select[name=executorRouteStrategy] option[value='+ row.executorRouteStrategy +']').prop('selected', true); + $("#updateModal .form input[name='childJobId']").val( row.childJobId ); + $('#updateModal .form select[name=misfireStrategy] option[value='+ row.misfireStrategy +']').prop('selected', true); + $('#updateModal .form select[name=executorBlockStrategy] option[value='+ row.executorBlockStrategy +']').prop('selected', true); + $("#updateModal .form input[name='executorTimeout']").val( row.executorTimeout ); + $("#updateModal .form input[name='executorFailRetryCount']").val( row.executorFailRetryCount ); + + // show + $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var updateModalValidate = $("#updateModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + + rules : { + jobDesc : { + required : true, + maxlength: 50 + }, + author : { + required : true + } + }, + messages : { + jobDesc : { + required : I18n.system_please_input + I18n.jobinfo_field_jobdesc + }, + author : { + required : I18n.system_please_input + I18n.jobinfo_field_author + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + + // process executorTimeout + executorFailRetryCount + var executorTimeout = $("#updateModal .form input[name='executorTimeout']").val(); + if(!/^\d+$/.test(executorTimeout)) { + executorTimeout = 0; + } + $("#updateModal .form input[name='executorTimeout']").val(executorTimeout); + var executorFailRetryCount = $("#updateModal .form input[name='executorFailRetryCount']").val(); + if(!/^\d+$/.test(executorFailRetryCount)) { + executorFailRetryCount = 0; + } + $("#updateModal .form input[name='executorFailRetryCount']").val(executorFailRetryCount); + + + // process schedule_conf + var scheduleType = $("#updateModal .form select[name='scheduleType']").val(); + var scheduleConf; + if (scheduleType == 'CRON') { + scheduleConf = $("#updateModal .form input[name='cronGen_display']").val(); + } else if (scheduleType == 'FIX_RATE') { + scheduleConf = $("#updateModal .form input[name='schedule_conf_FIX_RATE']").val(); + } else if (scheduleType == 'FIX_DELAY') { + scheduleConf = $("#updateModal .form input[name='schedule_conf_FIX_DELAY']").val(); + } + $("#updateModal .form input[name='scheduleConf']").val( scheduleConf ); + + // post + $.post(base_url + "/jobinfo/update", $("#updateModal .form").serialize(), function(data, status) { + if (data.code == "200") { + $('#updateModal').modal('hide'); + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: I18n.system_update_suc , + icon: '1', + end: function(layero, index){ + //window.location.reload(); + jobTable.fnDraw(); + } + }); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_update_fail ), + icon: '2' + }); + } + }); + } + }); + $("#updateModal").on('hide.bs.modal', function () { + updateModalValidate.resetForm(); + $("#updateModal .form")[0].reset(); + $("#updateModal .form .form-group").removeClass("has-error"); + }); + + /** + * find title by name, GlueType + */ + function findGlueTypeTitle(glueType) { + var glueTypeTitle; + $("#addModal .form select[name=glueType] option").each(function () { + var name = $(this).val(); + var title = $(this).text(); + if (glueType == name) { + glueTypeTitle = title; + return false + } + }); + return glueTypeTitle; + } + + // job_copy + $("#job_list").on('click', '.job_copy',function() { + + var id = $(this).parents('ul').attr("_id"); + var row = tableData['key'+id]; + + // fill base + $('#addModal .form select[name=jobGroup] option[value='+ row.jobGroup +']').prop('selected', true); + $("#addModal .form input[name='jobDesc']").val( row.jobDesc ); + $("#addModal .form input[name='author']").val( row.author ); + $("#addModal .form input[name='alarmEmail']").val( row.alarmEmail ); + + // fill trigger + $('#addModal .form select[name=scheduleType] option[value='+ row.scheduleType +']').prop('selected', true); + $("#addModal .form input[name='scheduleConf']").val( row.scheduleConf ); + if (row.scheduleType == 'CRON') { + $("#addModal .form input[name='schedule_conf_CRON']").val( row.scheduleConf ); + } else if (row.scheduleType == 'FIX_RATE') { + $("#addModal .form input[name='schedule_conf_FIX_RATE']").val( row.scheduleConf ); + } else if (row.scheduleType == 'FIX_DELAY') { + $("#addModal .form input[name='schedule_conf_FIX_DELAY']").val( row.scheduleConf ); + } + + // 》init scheduleType + $("#addModal .form select[name=scheduleType]").change(); + + // fill job + $('#addModal .form select[name=glueType] option[value='+ row.glueType +']').prop('selected', true); + $("#addModal .form input[name='executorHandler']").val( row.executorHandler ); + $("#addModal .form textarea[name='executorParam']").val( row.executorParam ); + + // 》init glueType + $("#addModal .form select[name=glueType]").change(); + + // 》init-cronGen + $("#addModal .form input[name='schedule_conf_CRON']").show().siblings().remove(); + $("#addModal .form input[name='schedule_conf_CRON']").cronGen({}); + + // fill advanced + $('#addModal .form select[name=executorRouteStrategy] option[value='+ row.executorRouteStrategy +']').prop('selected', true); + $("#addModal .form input[name='childJobId']").val( row.childJobId ); + $('#addModal .form select[name=misfireStrategy] option[value='+ row.misfireStrategy +']').prop('selected', true); + $('#addModal .form select[name=executorBlockStrategy] option[value='+ row.executorBlockStrategy +']').prop('selected', true); + $("#addModal .form input[name='executorTimeout']").val( row.executorTimeout ); + $("#addModal .form input[name='executorFailRetryCount']").val( row.executorFailRetryCount ); + + // show + $('#addModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js new file mode 100644 index 0000000..ddefd46 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js @@ -0,0 +1,91 @@ +$(function() { + + // trigger fail, end + if ( !(triggerCode == 200 || handleCode != 0) ) { + $('#logConsoleRunning').hide(); + $('#logConsole').append('<span style="color: red;">'+ I18n.joblog_rolling_log_triggerfail +'</span>'); + return; + } + + // pull log + var fromLineNum = 1; // [from, to], start as 1 + var pullFailCount = 0; + function pullLog() { + // pullFailCount, max=20 + if (pullFailCount++ > 20) { + logRunStop('<span style="color: red;">'+ I18n.joblog_rolling_log_failoften +'</span>'); + return; + } + + // load + console.log("pullLog, fromLineNum:" + fromLineNum); + + $.ajax({ + type : 'POST', + async: false, // sync, make log ordered + url : base_url + '/joblog/logDetailCat', + data : { + "executorAddress":executorAddress, + "triggerTime":triggerTime, + "logId":logId, + "fromLineNum":fromLineNum + }, + dataType : "json", + success : function(data){ + + if (data.code == 200) { + if (!data.content) { + console.log('pullLog fail'); + return; + } + if (fromLineNum != data.content.fromLineNum) { + console.log('pullLog fromLineNum not match'); + return; + } + if (fromLineNum > data.content.toLineNum ) { + console.log('pullLog already line-end'); + + // valid end + if (data.content.end) { + logRunStop('<br><span style="color: green;">[Rolling Log Finish]</span>'); + return; + } + + return; + } + + // append content + fromLineNum = data.content.toLineNum + 1; + $('#logConsole').append(data.content.logContent); + pullFailCount = 0; + + // scroll to bottom + scrollTo(0, document.body.scrollHeight); // $('#logConsolePre').scrollTop( document.body.scrollHeight + 300 ); + + } else { + console.log('pullLog fail:'+data.msg); + } + } + }); + } + + // pull first page + pullLog(); + + // handler already callback, end + if (handleCode > 0) { + logRunStop('<br><span style="color: green;">[Load Log Finish]</span>'); + return; + } + + // round until end + var logRun = setInterval(function () { + pullLog() + }, 3000); + function logRunStop(content){ + $('#logConsoleRunning').hide(); + logRun = window.clearInterval(logRun); + $('#logConsole').append(content); + } + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js new file mode 100644 index 0000000..e0fc3f2 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js @@ -0,0 +1,396 @@ +$(function() { + + // jobGroup change, job list init and select + $("#jobGroup").on("change", function () { + var jobGroup = $(this).children('option:selected').val(); + $.ajax({ + type : 'POST', + async: false, // async, avoid js invoke pagelist before jobId data init + url : base_url + '/joblog/getJobsByGroup', + data : {"jobGroup":jobGroup}, + dataType : "json", + success : function(data){ + if (data.code == 200) { + $("#jobId").html( '<option value="0" >'+ I18n.system_all +'</option>' ); + $.each(data.content, function (n, value) { + $("#jobId").append('<option value="' + value.id + '" >' + value.jobDesc + '</option>'); + }); + if ($("#jobId").attr("paramVal")){ + $("#jobId").find("option[value='" + $("#jobId").attr("paramVal") + "']").attr("selected",true); + } + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_api_error ), + icon: '2' + }); + } + }, + }); + }); + if ($("#jobGroup").attr("paramVal")){ + $("#jobGroup").find("option[value='" + $("#jobGroup").attr("paramVal") + "']").attr("selected",true); + $("#jobGroup").change(); + } + + // filter Time + var rangesConf = {}; + rangesConf[I18n.daterangepicker_ranges_recent_hour] = [moment().subtract(1, 'hours'), moment()]; + rangesConf[I18n.daterangepicker_ranges_today] = [moment().startOf('day'), moment().endOf('day')]; + rangesConf[I18n.daterangepicker_ranges_yesterday] = [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')]; + rangesConf[I18n.daterangepicker_ranges_this_month] = [moment().startOf('month'), moment().endOf('month')]; + rangesConf[I18n.daterangepicker_ranges_last_month] = [moment().subtract(1, 'months').startOf('month'), moment().subtract(1, 'months').endOf('month')]; + rangesConf[I18n.daterangepicker_ranges_recent_week] = [moment().subtract(1, 'weeks').startOf('day'), moment().endOf('day')]; + rangesConf[I18n.daterangepicker_ranges_recent_month] = [moment().subtract(1, 'months').startOf('day'), moment().endOf('day')]; + + $('#filterTime').daterangepicker({ + autoApply:false, + singleDatePicker:false, + showDropdowns:false, // 是否显示年月选择条件 + timePicker: true, // 是否显示小时和分钟选择条件 + timePickerIncrement: 10, // 时间的增量,单位为分钟 + timePicker24Hour : true, + opens : 'left', //日期选择框的弹出位置 + ranges: rangesConf, + locale : { + format: 'YYYY-MM-DD HH:mm:ss', + separator : ' - ', + customRangeLabel : I18n.daterangepicker_custom_name , + applyLabel : I18n.system_ok , + cancelLabel : I18n.system_cancel , + fromLabel : I18n.daterangepicker_custom_starttime , + toLabel : I18n.daterangepicker_custom_endtime , + daysOfWeek : I18n.daterangepicker_custom_daysofweek.split(',') , // '日', '一', '二', '三', '四', '五', '六' + monthNames : I18n.daterangepicker_custom_monthnames.split(',') , // '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月' + firstDay : 1 + }, + startDate: rangesConf[I18n.daterangepicker_ranges_today][0], + endDate: rangesConf[I18n.daterangepicker_ranges_today][1] + }); + + // init date tables + var logTable = $("#joblog_list").dataTable({ + "deferRender": true, + "processing" : true, + "serverSide": true, + "ajax": { + url: base_url + "/joblog/pageList" , + type:"post", + data : function ( d ) { + var obj = {}; + obj.jobGroup = $('#jobGroup').val(); + obj.jobId = $('#jobId').val(); + obj.logStatus = $('#logStatus').val(); + obj.filterTime = $('#filterTime').val(); + obj.start = d.start; + obj.length = d.length; + return obj; + } + }, + "searching": false, + "ordering": false, + //"scrollX": false, + "columns": [ + { + "data": 'jobId', + "visible" : true, + "width":'10%', + "render": function ( data, type, row ) { + + var jobhandler = ''; + if (row.executorHandler) { + jobhandler = "<br>JobHandler:" + row.executorHandler; + } + + var temp = ''; + temp += I18n.joblog_field_executorAddress + ':' + (row.executorAddress?row.executorAddress:''); + temp += jobhandler; + temp += '<br>'+ I18n.jobinfo_field_executorparam +':' + row.executorParam; + + return '<a class="logTips" href="javascript:;" >'+ row.jobId +'<span style="display:none;">'+ temp +'</span></a>'; + } + }, + { "data": 'jobGroup', "visible" : false}, + { + "data": 'triggerTime', + "width":'20%', + "render": function ( data, type, row ) { + return data?moment(data).format("YYYY-MM-DD HH:mm:ss"):""; + } + }, + { + "data": 'triggerCode', + "width":'10%', + "render": function ( data, type, row ) { + var html = data; + if (data == 200) { + html = '<span style="color: green">'+ I18n.system_success +'</span>'; + } else if (data == 500) { + html = '<span style="color: red">'+ I18n.system_fail +'</span>'; + } else if (data == 0) { + html = ''; + } + return html; + } + }, + { + "data": 'triggerMsg', + "width":'10%', + "render": function ( data, type, row ) { + return data?'<a class="logTips" href="javascript:;" >'+ I18n.system_show +'<span style="display:none;">'+ data +'</span></a>':I18n.system_empty; + } + }, + { + "data": 'handleTime', + "width":'20%', + "render": function ( data, type, row ) { + return data?moment(data).format("YYYY-MM-DD HH:mm:ss"):""; + } + }, + { + "data": 'handleCode', + "width":'10%', + "render": function ( data, type, row ) { + var html = data; + if (data == 200) { + html = '<span style="color: green">'+ I18n.joblog_handleCode_200 +'</span>'; + } else if (data == 500) { + html = '<span style="color: red">'+ I18n.joblog_handleCode_500 +'</span>'; + } else if (data == 502) { + html = '<span style="color: red">'+ I18n.joblog_handleCode_502 +'</span>'; + } else if (data == 0) { + html = ''; + } + return html; + } + }, + { + "data": 'handleMsg', + "width":'10%', + "render": function ( data, type, row ) { + return data?'<a class="logTips" href="javascript:;" >'+ I18n.system_show +'<span style="display:none;">'+ data +'</span></a>':I18n.system_empty; + } + }, + { + "data": 'handleMsg' , + "bSortable": false, + "width":'10%', + "render": function ( data, type, row ) { + // better support expression or string, not function + return function () { + if (row.triggerCode == 200 || row.handleCode != 0){ + + /*var temp = '<a href="javascript:;" class="logDetail" _id="'+ row.id +'">'+ I18n.joblog_rolling_log +'</a>'; + if(row.handleCode == 0){ + temp += '<br><a href="javascript:;" class="logKill" _id="'+ row.id +'" style="color: red;" >'+ I18n.joblog_kill_log +'</a>'; + }*/ + //return temp; + + var logKillDiv = ''; + if(row.handleCode == 0){ + logKillDiv = ' <li class="divider"></li>\n' + + ' <li><a href="javascript:void(0);" class="logKill" _id="'+ row.id +'" >'+ I18n.joblog_kill_log +'</a></li>\n'; + } + + var html = '<div class="btn-group">\n' + + ' <button type="button" class="btn btn-primary btn-sm">'+ I18n.system_opt +'</button>\n' + + ' <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">\n' + + ' <span class="caret"></span>\n' + + ' <span class="sr-only">Toggle Dropdown</span>\n' + + ' </button>\n' + + ' <ul class="dropdown-menu" role="menu" _id="'+ row.id +'" >\n' + + ' <li><a href="javascript:void(0);" class="logDetail" _id="'+ row.id +'" >'+ I18n.joblog_rolling_log +'</a></li>\n' + + logKillDiv + + ' </ul>\n' + + ' </div>'; + + return html; + } + return null; + } + } + } + ], + "language" : { + "sProcessing" : I18n.dataTable_sProcessing , + "sLengthMenu" : I18n.dataTable_sLengthMenu , + "sZeroRecords" : I18n.dataTable_sZeroRecords , + "sInfo" : I18n.dataTable_sInfo , + "sInfoEmpty" : I18n.dataTable_sInfoEmpty , + "sInfoFiltered" : I18n.dataTable_sInfoFiltered , + "sInfoPostFix" : "", + "sSearch" : I18n.dataTable_sSearch , + "sUrl" : "", + "sEmptyTable" : I18n.dataTable_sEmptyTable , + "sLoadingRecords" : I18n.dataTable_sLoadingRecords , + "sInfoThousands" : ",", + "oPaginate" : { + "sFirst" : I18n.dataTable_sFirst , + "sPrevious" : I18n.dataTable_sPrevious , + "sNext" : I18n.dataTable_sNext , + "sLast" : I18n.dataTable_sLast + }, + "oAria" : { + "sSortAscending" : I18n.dataTable_sSortAscending , + "sSortDescending" : I18n.dataTable_sSortDescending + } + } + }); + logTable.on('xhr.dt',function(e, settings, json, xhr) { + if (json.code && json.code != 200) { + layer.msg( json.msg || I18n.system_api_error ); + } + }); + + // logTips alert + $('#joblog_list').on('click', '.logTips', function(){ + var msg = $(this).find('span').html(); + ComAlertTec.show(msg); + }); + + // search Btn + $('#searchBtn').on('click', function(){ + logTable.fnDraw(); + }); + + // logDetail look + $('#joblog_list').on('click', '.logDetail', function(){ + var _id = $(this).attr('_id'); + + window.open(base_url + '/joblog/logDetailPage?id=' + _id); + return; + }); + + /** + * log Kill + */ + $('#joblog_list').on('click', '.logKill', function(){ + var _id = $(this).attr('_id'); + + layer.confirm( (I18n.system_ok + I18n.joblog_kill_log + '?'), { + icon: 3, + title: I18n.system_tips , + btn: [ I18n.system_ok, I18n.system_cancel ] + }, function(index){ + layer.close(index); + + $.ajax({ + type : 'POST', + url : base_url + '/joblog/logKill', + data : {"id":_id}, + dataType : "json", + success : function(data){ + if (data.code == 200) { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: I18n.system_opt_suc , + icon: '1', + end: function(layero, index){ + logTable.fnDraw(); + } + }); + } else { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_opt_fail ), + icon: '2' + }); + } + }, + }); + }); + + }); + + /** + * clear Log + */ + $('#clearLog').on('click', function(){ + + var jobGroup = $('#jobGroup').val(); + var jobId = $('#jobId').val(); + + var jobGroupText = $("#jobGroup").find("option:selected").text(); + var jobIdText = $("#jobId").find("option:selected").text(); + + $('#clearLogModal input[name=jobGroup]').val(jobGroup); + $('#clearLogModal input[name=jobId]').val(jobId); + + $('#clearLogModal .jobGroupText').val(jobGroupText); + $('#clearLogModal .jobIdText').val(jobIdText); + + $('#clearLogModal').modal('show'); + + }); + $("#clearLogModal .ok").on('click', function(){ + $.post(base_url + "/joblog/clearLog", $("#clearLogModal .form").serialize(), function(data, status) { + if (data.code == "200") { + $('#clearLogModal').modal('hide'); + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (I18n.joblog_clean_log + I18n.system_success) , + icon: '1', + end: function(layero, index){ + logTable.fnDraw(); + } + }); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || (I18n.joblog_clean_log + I18n.system_fail) ), + icon: '2' + }); + } + }); + }); + $("#clearLogModal").on('hide.bs.modal', function () { + $("#clearLogModal .form")[0].reset(); + }); + +}); + + +// Com Alert by Tec theme +var ComAlertTec = { + html:function(){ + var html = + '<div class="modal fade" id="ComAlertTec" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">' + + ' <div class="modal-dialog modal-lg-">' + + ' <div class="modal-content-tec">' + + ' <div class="modal-body">' + + ' <div class="alert" style="color:#fff;word-wrap: break-word;">' + + ' </div>' + + ' </div>' + + ' <div class="modal-footer">' + + ' <div class="text-center" >' + + ' <button type="button" class="btn btn-info ok" data-dismiss="modal" >'+ I18n.system_ok +'</button>' + + ' </div>' + + ' </div>' + + ' </div>' + + ' </div>' + + '</div>'; + return html; + }, + show:function(msg, callback){ + // dom init + if ($('#ComAlertTec').length == 0){ + $('body').append(ComAlertTec.html()); + } + + // init com alert + $('#ComAlertTec .alert').html(msg); + $('#ComAlertTec').modal('show'); + + $('#ComAlertTec .ok').click(function(){ + $('#ComAlertTec').modal('hide'); + if(typeof callback == 'function') { + callback(); + } + }); + } +}; diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js new file mode 100644 index 0000000..a420378 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js @@ -0,0 +1,66 @@ +$(function(){ + + // input iCheck + $('input').iCheck({ + checkboxClass: 'icheckbox_square-blue', + radioClass: 'iradio_square-blue', + increaseArea: '20%' // optional + }); + + // login Form Valid + var loginFormValid = $("#loginForm").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + userName : { + required : true , + minlength: 4, + maxlength: 18 + }, + password : { + required : true , + minlength: 4, + maxlength: 18 + } + }, + messages : { + userName : { + required : I18n.login_username_empty, + minlength : I18n.login_username_lt_4 + }, + password : { + required : I18n.login_password_empty , + minlength : I18n.login_password_lt_4 + /*,maxlength:"登录密码不应超过18位"*/ + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + $.post(base_url + "/login", $("#loginForm").serialize(), function(data, status) { + if (data.code == "200") { + layer.msg( I18n.login_success ); + setTimeout(function(){ + window.location.href = base_url + "/"; + }, 500); + } else { + layer.open({ + title: I18n.system_tips, + btn: [ I18n.system_ok ], + content: (data.msg || I18n.login_fail ), + icon: '2' + }); + } + }); + } + }); +}); \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js b/xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js new file mode 100644 index 0000000..48d3f30 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js @@ -0,0 +1,328 @@ +$(function() { + + // init date tables + var userListTable = $("#user_list").dataTable({ + "deferRender": true, + "processing" : true, + "serverSide": true, + "ajax": { + url: base_url + "/user/pageList", + type:"post", + data : function ( d ) { + var obj = {}; + obj.username = $('#username').val(); + obj.role = $('#role').val(); + obj.start = d.start; + obj.length = d.length; + return obj; + } + }, + "searching": false, + "ordering": false, + //"scrollX": true, // scroll x,close self-adaption + "columns": [ + { + "data": 'id', + "visible" : false, + "width":'10%' + }, + { + "data": 'username', + "visible" : true, + "width":'20%' + }, + { + "data": 'password', + "visible" : false, + "width":'20%', + "render": function ( data, type, row ) { + return '*********'; + } + }, + { + "data": 'role', + "visible" : true, + "width":'10%', + "render": function ( data, type, row ) { + if (data == 1) { + return I18n.user_role_admin + } else { + return I18n.user_role_normal + } + } + }, + { + "data": 'permission', + "width":'10%', + "visible" : false + }, + { + "data": I18n.system_opt , + "width":'15%', + "render": function ( data, type, row ) { + return function(){ + // html + tableData['key'+row.id] = row; + var html = '<p id="'+ row.id +'" >'+ + '<button class="btn btn-warning btn-xs update" type="button">'+ I18n.system_opt_edit +'</button> '+ + '<button class="btn btn-danger btn-xs delete" type="button">'+ I18n.system_opt_del +'</button> '+ + '</p>'; + + return html; + }; + } + } + ], + "language" : { + "sProcessing" : I18n.dataTable_sProcessing , + "sLengthMenu" : I18n.dataTable_sLengthMenu , + "sZeroRecords" : I18n.dataTable_sZeroRecords , + "sInfo" : I18n.dataTable_sInfo , + "sInfoEmpty" : I18n.dataTable_sInfoEmpty , + "sInfoFiltered" : I18n.dataTable_sInfoFiltered , + "sInfoPostFix" : "", + "sSearch" : I18n.dataTable_sSearch , + "sUrl" : "", + "sEmptyTable" : I18n.dataTable_sEmptyTable , + "sLoadingRecords" : I18n.dataTable_sLoadingRecords , + "sInfoThousands" : ",", + "oPaginate" : { + "sFirst" : I18n.dataTable_sFirst , + "sPrevious" : I18n.dataTable_sPrevious , + "sNext" : I18n.dataTable_sNext , + "sLast" : I18n.dataTable_sLast + }, + "oAria" : { + "sSortAscending" : I18n.dataTable_sSortAscending , + "sSortDescending" : I18n.dataTable_sSortDescending + } + } + }); + + // table data + var tableData = {}; + + // search btn + $('#searchBtn').on('click', function(){ + userListTable.fnDraw(); + }); + + // job operate + $("#user_list").on('click', '.delete',function() { + var id = $(this).parent('p').attr("id"); + + layer.confirm( I18n.system_ok + I18n.system_opt_del + '?', { + icon: 3, + title: I18n.system_tips , + btn: [ I18n.system_ok, I18n.system_cancel ] + }, function(index){ + layer.close(index); + + $.ajax({ + type : 'POST', + url : base_url + "/user/remove", + data : { + "id" : id + }, + dataType : "json", + success : function(data){ + if (data.code == 200) { + layer.msg( I18n.system_success ); + userListTable.fnDraw(false); + } else { + layer.msg( data.msg || I18n.system_opt_del + I18n.system_fail ); + } + } + }); + }); + }); + + // add role + $("#addModal .form input[name=role]").change(function () { + var role = $(this).val(); + if (role == 1) { + $("#addModal .form input[name=permission]").parents('.form-group').hide(); + } else { + $("#addModal .form input[name=permission]").parents('.form-group').show(); + } + $("#addModal .form input[name='permission']").prop("checked",false); + }); + + jQuery.validator.addMethod("myValid01", function(value, element) { + var length = value.length; + var valid = /^[a-z][a-z0-9]*$/; + return this.optional(element) || valid.test(value); + }, I18n.user_username_valid ); + + // add + $(".add").click(function(){ + $('#addModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var addModalValidate = $("#addModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + rules : { + username : { + required : true, + rangelength:[4, 20], + myValid01: true + }, + password : { + required : true, + rangelength:[4, 20] + } + }, + messages : { + username : { + required : I18n.system_please_input + I18n.user_username, + rangelength: I18n.system_lengh_limit + "[4-20]" + }, + password : { + required : I18n.system_please_input + I18n.user_password, + rangelength: I18n.system_lengh_limit + "[4-20]" + } + }, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + + var permissionArr = []; + $("#addModal .form input[name=permission]:checked").each(function(){ + permissionArr.push($(this).val()); + }); + + var paramData = { + "username": $("#addModal .form input[name=username]").val(), + "password": $("#addModal .form input[name=password]").val(), + "role": $("#addModal .form input[name=role]:checked").val(), + "permission": permissionArr.join(',') + }; + + $.post(base_url + "/user/add", paramData, function(data, status) { + if (data.code == "200") { + $('#addModal').modal('hide'); + + layer.msg( I18n.system_add_suc ); + userListTable.fnDraw(); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_add_fail), + icon: '2' + }); + } + }); + } + }); + $("#addModal").on('hide.bs.modal', function () { + $("#addModal .form")[0].reset(); + addModalValidate.resetForm(); + $("#addModal .form .form-group").removeClass("has-error"); + $(".remote_panel").show(); // remote + + $("#addModal .form input[name=permission]").parents('.form-group').show(); + }); + + // update role + $("#updateModal .form input[name=role]").change(function () { + var role = $(this).val(); + if (role == 1) { + $("#updateModal .form input[name=permission]").parents('.form-group').hide(); + } else { + $("#updateModal .form input[name=permission]").parents('.form-group').show(); + } + $("#updateModal .form input[name='permission']").prop("checked",false); + }); + + // update + $("#user_list").on('click', '.update',function() { + + var id = $(this).parent('p').attr("id"); + var row = tableData['key'+id]; + + // base data + $("#updateModal .form input[name='id']").val( row.id ); + $("#updateModal .form input[name='username']").val( row.username ); + $("#updateModal .form input[name='password']").val( '' ); + $("#updateModal .form input[name='role'][value='"+ row.role +"']").click(); + var permissionArr = []; + if (row.permission) { + permissionArr = row.permission.split(","); + } + $("#updateModal .form input[name='permission']").each(function () { + if($.inArray($(this).val(), permissionArr) > -1) { + $(this).prop("checked",true); + } else { + $(this).prop("checked",false); + } + }); + + // show + $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show'); + }); + var updateModalValidate = $("#updateModal .form").validate({ + errorElement : 'span', + errorClass : 'help-block', + focusInvalid : true, + highlight : function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + success : function(label) { + label.closest('.form-group').removeClass('has-error'); + label.remove(); + }, + errorPlacement : function(error, element) { + element.parent('div').append(error); + }, + submitHandler : function(form) { + + var permissionArr =[]; + $("#updateModal .form input[name=permission]:checked").each(function(){ + permissionArr.push($(this).val()); + }); + + var paramData = { + "id": $("#updateModal .form input[name=id]").val(), + "username": $("#updateModal .form input[name=username]").val(), + "password": $("#updateModal .form input[name=password]").val(), + "role": $("#updateModal .form input[name=role]:checked").val(), + "permission": permissionArr.join(',') + }; + + $.post(base_url + "/user/update", paramData, function(data, status) { + if (data.code == "200") { + $('#updateModal').modal('hide'); + + layer.msg( I18n.system_update_suc ); + userListTable.fnDraw(); + } else { + layer.open({ + title: I18n.system_tips , + btn: [ I18n.system_ok ], + content: (data.msg || I18n.system_update_fail), + icon: '2' + }); + } + }); + } + }); + $("#updateModal").on('hide.bs.modal', function () { + $("#updateModal .form")[0].reset(); + updateModalValidate.resetForm(); + $("#updateModal .form .form-group").removeClass("has-error"); + $(".remote_panel").show(); // remote + + $("#updateModal .form input[name=permission]").parents('.form-group').show(); + }); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js new file mode 100644 index 0000000..d27a9ec --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js @@ -0,0 +1,41 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var WORD = /[\w$]+/, RANGE = 500; + + CodeMirror.registerHelper("hint", "anyword", function(editor, options) { + var word = options && options.word || WORD; + var range = options && options.range || RANGE; + var cur = editor.getCursor(), curLine = editor.getLine(cur.line); + var end = cur.ch, start = end; + while (start && word.test(curLine.charAt(start - 1))) --start; + var curWord = start != end && curLine.slice(start, end); + + var list = options && options.list || [], seen = {}; + var re = new RegExp(word.source, "g"); + for (var dir = -1; dir <= 1; dir += 2) { + var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir; + for (; line != endLine; line += dir) { + var text = editor.getLine(line), m; + while (m = re.exec(text)) { + if (line == cur.line && m[0] === curWord) continue; + if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) { + seen[m[0]] = true; + list.push(m[0]); + } + } + } + } + return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; + }); +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css new file mode 100644 index 0000000..5617ccc --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css @@ -0,0 +1,36 @@ +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js new file mode 100644 index 0000000..5f6664b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js @@ -0,0 +1,434 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var HINT_ELEMENT_CLASS = "CodeMirror-hint"; + var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + + // This is the old interface, kept around for now to stay + // backwards-compatible. + CodeMirror.showHint = function(cm, getHints, options) { + if (!getHints) return cm.showHint(options); + if (options && options.async) getHints.async = true; + var newOpts = {hint: getHints}; + if (options) for (var prop in options) newOpts[prop] = options[prop]; + return cm.showHint(newOpts); + }; + + CodeMirror.defineExtension("showHint", function(options) { + options = parseOptions(this, this.getCursor("start"), options); + var selections = this.listSelections() + if (selections.length > 1) return; + // By default, don't allow completion when something is selected. + // A hint function can have a `supportsSelection` property to + // indicate that it can handle selections. + if (this.somethingSelected()) { + if (!options.hint.supportsSelection) return; + // Don't try with cross-line selections + for (var i = 0; i < selections.length; i++) + if (selections[i].head.line != selections[i].anchor.line) return; + } + + if (this.state.completionActive) this.state.completionActive.close(); + var completion = this.state.completionActive = new Completion(this, options); + if (!completion.options.hint) return; + + CodeMirror.signal(this, "startCompletion", this); + completion.update(true); + }); + + function Completion(cm, options) { + this.cm = cm; + this.options = options; + this.widget = null; + this.debounce = 0; + this.tick = 0; + this.startPos = this.cm.getCursor("start"); + this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; + + var self = this; + cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); + } + + var requestAnimationFrame = window.requestAnimationFrame || function(fn) { + return setTimeout(fn, 1000/60); + }; + var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + + Completion.prototype = { + close: function() { + if (!this.active()) return; + this.cm.state.completionActive = null; + this.tick = null; + this.cm.off("cursorActivity", this.activityFunc); + + if (this.widget && this.data) CodeMirror.signal(this.data, "close"); + if (this.widget) this.widget.close(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function() { + return this.cm.state.completionActive == this; + }, + + pick: function(data, i) { + var completion = data.list[i]; + if (completion.hint) completion.hint(this.cm, data, completion); + else this.cm.replaceRange(getText(completion), completion.from || data.from, + completion.to || data.to, "complete"); + CodeMirror.signal(data, "pick", completion); + this.close(); + }, + + cursorActivity: function() { + if (this.debounce) { + cancelAnimationFrame(this.debounce); + this.debounce = 0; + } + + var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); + if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || + pos.ch < this.startPos.ch || this.cm.somethingSelected() || + (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { + this.close(); + } else { + var self = this; + this.debounce = requestAnimationFrame(function() {self.update();}); + if (this.widget) this.widget.disable(); + } + }, + + update: function(first) { + if (this.tick == null) return + var self = this, myTick = ++this.tick + fetchHints(this.options.hint, this.cm, this.options, function(data) { + if (self.tick == myTick) self.finishUpdate(data, first) + }) + }, + + finishUpdate: function(data, first) { + if (this.data) CodeMirror.signal(this.data, "update"); + + var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); + if (this.widget) this.widget.close(); + + this.data = data; + + if (data && data.list.length) { + if (picked && data.list.length == 1) { + this.pick(data, 0); + } else { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + } + } + } + }; + + function parseOptions(cm, pos, options) { + var editor = cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) + return out; + } + + function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; + } + + function buildKeyMap(completion, handle) { + var baseMap = { + Up: function() {handle.moveFocus(-1);}, + Down: function() {handle.moveFocus(1);}, + PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, + PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, + Home: function() {handle.setFocus(0);}, + End: function() {handle.setFocus(handle.length - 1);}, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + var custom = completion.options.customKeys; + var ourMap = custom ? {} : baseMap; + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function(cm) { return val(cm, handle); }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (custom) + for (var key in custom) if (custom.hasOwnProperty(key)) + addBinding(key, custom[key]); + var extra = completion.options.extraKeys; + if (extra) + for (var key in extra) if (extra.hasOwnProperty(key)) + addBinding(key, extra[key]); + return ourMap; + } + + function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } + } + + function Widget(completion, data) { + this.completion = completion; + this.data = data; + this.picked = false; + var widget = this, cm = completion.cm; + + var hints = this.hints = document.createElement("ul"); + var theme = completion.cm.options.theme; + hints.className = "CodeMirror-hints " + theme; + this.selectedHint = data.selectedHint || 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); + var left = pos.left, top = pos.bottom, below = true; + hints.style.left = left + "px"; + hints.style.top = top + "px"; + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + (completion.options.container || document.body).appendChild(hints); + var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; + var scrolls = hints.scrollHeight > hints.clientHeight + 1 + var startScroll = cm.getScrollInfo(); + + if (overlapY > 0) { + var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = pos.top - height) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.right - winW; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX) + "px"; + } + if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling) + node.style.paddingRight = cm.display.nativeBarWidth + "px" + + cm.addKeyMap(this.keyMap = buildKeyMap(completion, { + moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, + setFocus: function(n) { widget.changeActive(n); }, + menuSize: function() { return widget.screenAmount(); }, + length: completions.length, + close: function() { completion.close(); }, + pick: function() { widget.pick(); }, + data: data + })); + + if (completion.options.closeOnUnfocus) { + var closingOnBlur; + cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); + cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); + } + + cm.on("scroll", this.onScroll = function() { + var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} + }); + + CodeMirror.on(hints, "click", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (completion.options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function() { + setTimeout(function(){cm.focus();}, 20); + }); + + CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); + return true; + } + + Widget.prototype = { + close: function() { + if (this.completion.widget != this) return; + this.completion.widget = null; + this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + disable: function() { + this.completion.cm.removeKeyMap(this.keyMap); + var widget = this; + this.keyMap = {Enter: function() { widget.picked = true; }}; + this.completion.cm.addKeyMap(this.keyMap); + }, + + pick: function() { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function(i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + if (node.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node.offsetTop - 3; + else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + screenAmount: function() { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + } + }; + + function applicableHelpers(cm, helpers) { + if (!cm.somethingSelected()) return helpers + var result = [] + for (var i = 0; i < helpers.length; i++) + if (helpers[i].supportsSelection) result.push(helpers[i]) + return result + } + + function fetchHints(hint, cm, options, callback) { + if (hint.async) { + hint(cm, callback, options) + } else { + var result = hint(cm, options) + if (result && result.then) result.then(callback) + else callback(result) + } + } + + function resolveAutoHints(cm, pos) { + var helpers = cm.getHelpers(pos, "hint"), words + if (helpers.length) { + var resolved = function(cm, callback, options) { + var app = applicableHelpers(cm, helpers); + function run(i) { + if (i == app.length) return callback(null) + fetchHints(app[i], cm, options, function(result) { + if (result && result.list.length > 0) callback(result) + else run(i + 1) + }) + } + run(0) + } + resolved.async = true + resolved.supportsSelection = true + return resolved + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { + return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } + } else if (CodeMirror.hint.anyword) { + return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } + } else { + return function() {} + } + } + + CodeMirror.registerHelper("hint", "auto", { + resolve: resolveAutoHints + }); + + CodeMirror.registerHelper("hint", "fromList", function(cm, options) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur) + var term, from = CodeMirror.Pos(cur.line, token.start), to = cur + if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { + term = token.string.substr(0, cur.ch - token.start) + } else { + term = "" + from = cur + } + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, term.length) == term) + found.push(word); + } + + if (found.length) return {list: found, from: from, to: to}; + }); + + CodeMirror.commands.autocomplete = CodeMirror.showHint; + + var defaultOptions = { + hint: CodeMirror.hint.auto, + completeSingle: true, + alignWithWord: true, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnUnfocus: true, + completeOnSingleClick: true, + container: null, + customKeys: null, + extraKeys: null + }; + + CodeMirror.defineOption("hintOptions", null); +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css new file mode 100644 index 0000000..c7a8ae7 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css @@ -0,0 +1,346 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; + direction: ltr; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: -20px; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + padding: 0.1px; /* Force widget margins to stay inside of the container */ +} + +.CodeMirror-widget {} + +.CodeMirror-rtl pre { direction: rtl; } + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); +} + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js new file mode 100644 index 0000000..93c0ead --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js @@ -0,0 +1,9698 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// This is CodeMirror (https://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.CodeMirror = factory()); +}(this, (function () { 'use strict'; + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + var userAgent = navigator.userAgent; + var platform = navigator.platform; + + var gecko = /gecko\/\d/i.test(userAgent); + var ie_upto10 = /MSIE \d/.test(userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); + var edge = /Edge\/(\d+)/.exec(userAgent); + var ie = ie_upto10 || ie_11up || edge; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]); + var webkit = !edge && /WebKit\//.test(userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); + var chrome = !edge && /Chrome\//.test(userAgent); + var presto = /Opera\//.test(userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); + var phantom = /PhantomJS/.test(userAgent); + + var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); + var android = /Android/.test(userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); + var mac = ios || /Mac/.test(platform); + var chromeOS = /\bCrOS\b/.test(userAgent); + var windows = /win/i.test(platform); + + var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) { presto_version = Number(presto_version[1]); } + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } + + var rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + { e.removeChild(e.firstChild); } + return e + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e) + } + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) { e.className = className; } + if (style) { e.style.cssText = style; } + if (typeof content == "string") { e.appendChild(document.createTextNode(content)); } + else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]); } } + return e + } + // wrapper for elt, which removes the elt from the accessibility tree + function eltP(tag, content, className, style) { + var e = elt(tag, content, className, style); + e.setAttribute("role", "presentation"); + return e + } + + var range; + if (document.createRange) { range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r + }; } + else { range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r + }; } + + function contains(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + { child = child.parentNode; } + if (parent.contains) + { return parent.contains(child) } + do { + if (child.nodeType == 11) { child = child.host; } + if (child == parent) { return true } + } while (child = child.parentNode) + } + + function activeElt() { + // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. + // IE < 10 will throw when accessed while the page is loading or in an iframe. + // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. + var activeElement; + try { + activeElement = document.activeElement; + } catch(e) { + activeElement = document.body || null; + } + while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) + { activeElement = activeElement.shadowRoot.activeElement; } + return activeElement + } + + function addClass(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls; } + } + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i]; } } + return b + } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; } + else if (ie) // Suppress mysterious IE10 errors + { selectInput = function(node) { try { node.select(); } catch(_e) {} }; } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args)} + } + + function copyObj(obj, target, overwrite) { + if (!target) { target = {}; } + for (var prop in obj) + { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + { target[prop] = obj[prop]; } } + return target + } + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) { end = string.length; } + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + { return n + (end - i) } + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + } + + var Delayed = function() {this.id = null;}; + Delayed.prototype.set = function (ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + { if (array[i] == elt) { return i } } + return -1 + } + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = {toString: function(){return "CodeMirror.Pass"}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) { nextTab = string.length; } + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + { return pos + Math.min(skipped, goal - col) } + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) { return pos } + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + { spaceStrs.push(lst(spaceStrs) + " "); } + return spaceStrs[n] + } + + function lst(arr) { return arr[arr.length-1] } + + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i); } + return out + } + + function insertSorted(array, value, score) { + var pos = 0, priority = score(value); + while (pos < array.length && score(array[pos]) <= priority) { pos++; } + array.splice(pos, 0, value); + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) { copyObj(props, inst); } + return inst + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + function isWordCharBasic(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) + } + function isWordChar(ch, helper) { + if (!helper) { return isWordCharBasic(ch) } + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } + return helper.test(ch) + } + + function isEmpty(obj) { + for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } + return true + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } + + // Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. + function skipExtendingChars(str, pos, dir) { + while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir; } + return pos + } + + // Returns the value from the range [`from`; `to`] that satisfies + // `pred` and is closest to `from`. Assumes that at least `to` + // satisfies `pred`. Supports `from` being greater than `to`. + function findFirst(pred, from, to) { + // At any point we are certain `to` satisfies `pred`, don't know + // whether `from` does. + var dir = from > to ? -1 : 1; + for (;;) { + if (from == to) { return from } + var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF); + if (mid == from) { return pred(mid) ? from : to } + if (pred(mid)) { to = mid; } + else { from = mid + dir; } + } + } + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = eltP("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + var lines = eltP("div", [d.lineSpace], "CodeMirror-lines"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [lines], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true; } + + if (place) { + if (place.appendChild) { place.appendChild(d.wrapper); } + else { place(d.wrapper); } + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + input.init(d); + } + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } + var chunk = doc; + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break } + n -= sz; + } + } + return chunk.lines[n] + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function (line) { + var text = line.text; + if (n == end.line) { text = text.slice(0, end.ch); } + if (n == start.line) { text = text.slice(start.ch); } + out.push(text); + ++n; + }); + return out + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function (line) { out.push(line.text); }); // iter aborts when callback returns truthy value + return out + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) { for (var n = line; n; n = n.parent) { n.height += diff; } } + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) { return null } + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) { break } + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { + var child = chunk.children[i$1], ch = child.height; + if (h < ch) { chunk = child; continue outer } + h -= ch; + n += child.chunkSize(); + } + return n + } while (!chunk.lines) + var i = 0; + for (; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) { break } + h -= lh; + } + return n + i + } + + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)) + } + + // A Pos instance represents a position within the text. + function Pos(line, ch, sticky) { + if ( sticky === void 0 ) sticky = null; + + if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } + this.line = line; + this.ch = ch; + this.sticky = sticky; + } + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + function cmp(a, b) { return a.line - b.line || a.ch - b.ch } + + function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } + + function copyPos(x) {return Pos(x.line, x.ch)} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} + function clipPos(doc, pos) { + if (pos.line < doc.first) { return Pos(doc.first, 0) } + var last = doc.first + doc.size - 1; + if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } + return clipToLen(pos, getLine(doc, pos.line).text.length) + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } + else if (ch < 0) { return Pos(pos.line, 0) } + else { return pos } + } + function clipPosArray(doc, array) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]); } + return out + } + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + function seeReadOnlySpans() { + sawReadOnlySpans = true; + } + + function seeCollapsedSpans() { + sawCollapsedSpans = true; + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) { return span } + } } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + var r; + for (var i = 0; i < spans.length; ++i) + { if (spans[i] != span) { (r || (r = [])).push(spans[i]); } } + return r + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } } + return nw + } + function markedSpansAfter(old, endCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } } + return nw + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) { return null } + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) { return null } + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) { span.to = startCh; } + else if (sameLine) { span.to = found.to == null ? null : found.to + offset; } + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i$1 = 0; i$1 < last.length; ++i$1) { + var span$1 = last[i$1]; + if (span$1.to != null) { span$1.to += offset; } + if (span$1.from == null) { + var found$1 = getMarkedSpanFor(first, span$1.marker); + if (!found$1) { + span$1.from = offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } else { + span$1.from += offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } + } + // Make sure we didn't create any zero-length spans + if (first) { first = clearEmptySpans(first); } + if (last && last != first) { last = clearEmptySpans(last); } + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + { for (var i$2 = 0; i$2 < first.length; ++i$2) + { if (first[i$2].to == null) + { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)); } } } + for (var i$3 = 0; i$3 < gap; ++i$3) + { newMarkers.push(gapMarkers); } + newMarkers.push(last); + } + return newMarkers + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + { spans.splice(i--, 1); } + } + if (!spans.length) { return null } + return spans + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function (line) { + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + { (markers || (markers = [])).push(mark); } + } } + }); + if (!markers) { return null } + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + { newParts.push({from: p.from, to: m.from}); } + if (dto > 0 || !mk.inclusiveRight && !dto) + { newParts.push({from: m.to, to: p.to}); } + parts.splice.apply(parts, newParts); + j += newParts.length - 3; + } + } + return parts + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.detachLine(line); } + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.attachLine(line); } + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) { return lenDiff } + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) { return -fromCmp } + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) { return toCmp } + return b.id - a.id + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + { found = sp.marker; } + } } + return found + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } + + function collapsedSpanAround(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker; } + } } + return found + } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo$$1, from, to, marker) { + var line = getLine(doc, lineNo$$1); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) { continue } + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } + if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || + fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) + { return true } + } } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + { line = merged.find(-1, true).line; } + return line + } + + function visualLineEnd(line) { + var merged; + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return line + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line + ;(lines || (lines = [])).push(line); + } + return lines + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) { return lineN } + return lineNo(vis) + } + + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) { return lineN } + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) { return lineN } + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return lineNo(line) + 1 + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) { continue } + if (sp.from == null) { return true } + if (sp.marker.widgetNode) { continue } + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + { return true } + } } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) + } + if (span.marker.inclusiveRight && span.to == line.text.length) + { return true } + for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) { return true } + } + } + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) { break } + else { h += line.height; } + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i$1 = 0; i$1 < p.children.length; ++i$1) { + var cur = p.children[i$1]; + if (cur == chunk) { break } + else { h += cur.height; } + } + } + return h + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) { return 0 } + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found$1 = merged.find(0, true); + len -= cur.text.length - found$1.from.ch; + cur = found$1.to.line; + len += cur.text.length - found$1.to.ch; + } + return len + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function (line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) { return f(from, to, "ltr", 0) } + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i); + found = true; + } + } + if (!found) { f(from, to, "ltr"); } + } + + var bidiOther = null; + function getBidiPartAt(order, ch, sticky) { + var found; + bidiOther = null; + for (var i = 0; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < ch && cur.to > ch) { return i } + if (cur.to == ch) { + if (cur.from != cur.to && sticky == "before") { found = i; } + else { bidiOther = i; } + } + if (cur.from == ch) { + if (cur.from != cur.to && sticky != "before") { found = i; } + else { bidiOther = i; } + } + } + return found != null ? found : bidiOther + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6f9 + var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"; + function charType(code) { + if (code <= 0xf7) { return lowTypes.charAt(code) } + else if (0x590 <= code && code <= 0x5f4) { return "R" } + else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } + else if (0x6ee <= code && code <= 0x8ac) { return "r" } + else if (0x2000 <= code && code <= 0x200b) { return "w" } + else if (code == 0x200c) { return "b" } + else { return "L" } + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str, direction) { + var outerType = direction == "ltr" ? "L" : "R"; + + if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } + var len = str.length, types = []; + for (var i = 0; i < len; ++i) + { types.push(charType(str.charCodeAt(i))); } + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { + var type = types[i$1]; + if (type == "m") { types[i$1] = prev; } + else { prev = type; } + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { + var type$1 = types[i$2]; + if (type$1 == "1" && cur == "r") { types[i$2] = "n"; } + else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R"; } } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { + var type$2 = types[i$3]; + if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1"; } + else if (type$2 == "," && prev$1 == types[i$3+1] && + (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1; } + prev$1 = type$2; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i$4 = 0; i$4 < len; ++i$4) { + var type$3 = types[i$4]; + if (type$3 == ",") { types[i$4] = "N"; } + else if (type$3 == "%") { + var end = (void 0); + for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i$4; j < end; ++j) { types[j] = replace; } + i$4 = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { + var type$4 = types[i$5]; + if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L"; } + else if (isStrong.test(type$4)) { cur$1 = type$4; } + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i$6 = 0; i$6 < len; ++i$6) { + if (isNeutral.test(types[i$6])) { + var end$1 = (void 0); + for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} + var before = (i$6 ? types[i$6-1] : outerType) == "L"; + var after = (end$1 < len ? types[end$1] : outerType) == "L"; + var replace$1 = before == after ? (before ? "L" : "R") : outerType; + for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1; } + i$6 = end$1 - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i$7 = 0; i$7 < len;) { + if (countsAsLeft.test(types[i$7])) { + var start = i$7; + for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} + order.push(new BidiSpan(0, start, i$7)); + } else { + var pos = i$7, at = order.length; + for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} + for (var j$2 = pos; j$2 < i$7;) { + if (countsAsNum.test(types[j$2])) { + if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)); } + var nstart = j$2; + for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} + order.splice(at, 0, new BidiSpan(2, nstart, j$2)); + pos = j$2; + } else { ++j$2; } + } + if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)); } + } + } + if (direction == "ltr") { + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + } + + return direction == "rtl" ? order.reverse() : order + } + })(); + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line, direction) { + var order = line.order; + if (order == null) { order = line.order = bidiOrdering(line.text, direction); } + return order + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var noHandlers = []; + + var on = function(emitter, type, f) { + if (emitter.addEventListener) { + emitter.addEventListener(type, f, false); + } else if (emitter.attachEvent) { + emitter.attachEvent("on" + type, f); + } else { + var map$$1 = emitter._handlers || (emitter._handlers = {}); + map$$1[type] = (map$$1[type] || noHandlers).concat(f); + } + }; + + function getHandlers(emitter, type) { + return emitter._handlers && emitter._handlers[type] || noHandlers + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) { + emitter.removeEventListener(type, f, false); + } else if (emitter.detachEvent) { + emitter.detachEvent("on" + type, f); + } else { + var map$$1 = emitter._handlers, arr = map$$1 && map$$1[type]; + if (arr) { + var index = indexOf(arr, f); + if (index > -1) + { map$$1[type] = arr.slice(0, index).concat(arr.slice(index + 1)); } + } + } + } + + function signal(emitter, type /*, values...*/) { + var handlers = getHandlers(emitter, type); + if (!handlers.length) { return } + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args); } + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + { e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; } + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) { return } + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) + { set.push(arr[i]); } } + } + + function hasHandler(emitter, type) { + return getHandlers(emitter, type).length > 0 + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + function e_preventDefault(e) { + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } + } + function e_stopPropagation(e) { + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + + function e_target(e) {return e.target || e.srcElement} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) { b = 1; } + else if (e.button & 2) { b = 3; } + else if (e.button & 4) { b = 2; } + } + if (mac && e.ctrlKey && b == 1) { b = 3; } + return b + } + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) { return false } + var div = elt('div'); + return "draggable" in div || "dragDrop" in div + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); } + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) { return badBidiRects } + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + var r1 = range(txt, 1, 2).getBoundingClientRect(); + removeChildren(measure); + if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) + return badBidiRects = (r1.right - r0.right < 3) + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) { nl = string.length; } + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result + } : function (string) { return string.split(/\r\n?|\n/); }; + + var hasSelection = window.getSelection ? function (te) { + try { return te.selectionStart != te.selectionEnd } + catch(e) { return false } + } : function (te) { + var range$$1; + try {range$$1 = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range$$1 || range$$1.parentElement() != te) { return false } + return range$$1.compareEndPoints("StartToEnd", range$$1) != 0 + }; + + var hasCopyEvent = (function () { + var e = elt("div"); + if ("oncopy" in e) { return true } + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function" + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) { return badZoomedRects } + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 + } + + // Known modes, by name and by MIME + var modes = {}, mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + function defineMode(name, mode) { + if (arguments.length > 2) + { mode.dependencies = Array.prototype.slice.call(arguments, 2); } + modes[name] = mode; + } + + function defineMIME(mime, spec) { + mimeModes[mime] = spec; + } + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + function resolveMode(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") { found = {name: found}; } + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return resolveMode("application/xml") + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { + return resolveMode("application/json") + } + if (typeof spec == "string") { return {name: spec} } + else { return spec || {name: "null"} } + } + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + function getMode(options, spec) { + spec = resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) { return getMode(options, "text/plain") } + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) { continue } + if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop]; } + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) { modeObj.helperType = spec.helperType; } + if (spec.modeProps) { for (var prop$1 in spec.modeProps) + { modeObj[prop$1] = spec.modeProps[prop$1]; } } + + return modeObj + } + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = {}; + function extendMode(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + } + + function copyState(mode, state) { + if (state === true) { return state } + if (mode.copyState) { return mode.copyState(state) } + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) { val = val.concat([]); } + nstate[n] = val; + } + return nstate + } + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + function innerMode(mode, state) { + var info; + while (mode.innerMode) { + info = mode.innerMode(state); + if (!info || info.mode == mode) { break } + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state} + } + + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true + } + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = function(string, tabSize, lineOracle) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + this.lineOracle = lineOracle; + }; + + StringStream.prototype.eol = function () {return this.pos >= this.string.length}; + StringStream.prototype.sol = function () {return this.pos == this.lineStart}; + StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; + StringStream.prototype.next = function () { + if (this.pos < this.string.length) + { return this.string.charAt(this.pos++) } + }; + StringStream.prototype.eat = function (match) { + var ch = this.string.charAt(this.pos); + var ok; + if (typeof match == "string") { ok = ch == match; } + else { ok = ch && (match.test ? match.test(ch) : match(ch)); } + if (ok) {++this.pos; return ch} + }; + StringStream.prototype.eatWhile = function (match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start + }; + StringStream.prototype.eatSpace = function () { + var this$1 = this; + + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos; } + return this.pos > start + }; + StringStream.prototype.skipToEnd = function () {this.pos = this.string.length;}; + StringStream.prototype.skipTo = function (ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true} + }; + StringStream.prototype.backUp = function (n) {this.pos -= n;}; + StringStream.prototype.column = function () { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.indentation = function () { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.match = function (pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) { this.pos += pattern.length; } + return true + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) { return null } + if (match && consume !== false) { this.pos += match[0].length; } + return match + } + }; + StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; + StringStream.prototype.hideFirstChars = function (n, inner) { + this.lineStart += n; + try { return inner() } + finally { this.lineStart -= n; } + }; + StringStream.prototype.lookAhead = function (n) { + var oracle = this.lineOracle; + return oracle && oracle.lookAhead(n) + }; + StringStream.prototype.baseToken = function () { + var oracle = this.lineOracle; + return oracle && oracle.baseToken(this.pos) + }; + + var SavedContext = function(state, lookAhead) { + this.state = state; + this.lookAhead = lookAhead; + }; + + var Context = function(doc, state, line, lookAhead) { + this.state = state; + this.doc = doc; + this.line = line; + this.maxLookAhead = lookAhead || 0; + this.baseTokens = null; + this.baseTokenPos = 1; + }; + + Context.prototype.lookAhead = function (n) { + var line = this.doc.getLine(this.line + n); + if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n; } + return line + }; + + Context.prototype.baseToken = function (n) { + var this$1 = this; + + if (!this.baseTokens) { return null } + while (this.baseTokens[this.baseTokenPos] <= n) + { this$1.baseTokenPos += 2; } + var type = this.baseTokens[this.baseTokenPos + 1]; + return {type: type && type.replace(/( |^)overlay .*/, ""), + size: this.baseTokens[this.baseTokenPos] - n} + }; + + Context.prototype.nextLine = function () { + this.line++; + if (this.maxLookAhead > 0) { this.maxLookAhead--; } + }; + + Context.fromSaved = function (doc, saved, line) { + if (saved instanceof SavedContext) + { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } + else + { return new Context(doc, copyState(doc.mode, saved), line) } + }; + + Context.prototype.save = function (copy) { + var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state; + return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state + }; + + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, context, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, + lineClasses, forceToEnd); + var state = context.state; + + // Run overlays, adjust style array. + var loop = function ( o ) { + context.baseTokens = st; + var overlay = cm.state.overlays[o], i = 1, at = 0; + context.state = true; + runMode(cm, line.text, overlay.mode, context, function (end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + { st.splice(i, 1, end, st[i+1], i_end); } + i += 2; + at = Math.min(end, i_end); + } + if (!style) { return } + if (overlay.opaque) { + st.splice(start, i - start, end, "overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "overlay " + style; + } + } + }, lineClasses); + context.state = state; + context.baseTokens = null; + context.baseTokenPos = 1; + }; + + for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var context = getContextBefore(cm, lineNo(line)); + var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state); + var result = highlightLine(cm, line, context); + if (resetState) { context.state = resetState; } + line.stateAfter = context.save(!resetState); + line.styles = result.styles; + if (result.classes) { line.styleClasses = result.classes; } + else if (line.styleClasses) { line.styleClasses = null; } + if (updateFrontier === cm.doc.highlightFrontier) + { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier); } + } + return line.styles + } + + function getContextBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) { return new Context(doc, true, n) } + var start = findStartLine(cm, n, precise); + var saved = start > doc.first && getLine(doc, start - 1).stateAfter; + var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start); + + doc.iter(start, n, function (line) { + processLine(cm, line.text, context); + var pos = context.line; + line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null; + context.nextLine(); + }); + if (precise) { doc.modeFrontier = context.line; } + return context + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, context, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize, context); + stream.start = stream.pos = startAt || 0; + if (text == "") { callBlankLine(mode, context.state); } + while (!stream.eol()) { + readToken(mode, stream, context.state); + stream.start = stream.pos; + } + } + + function callBlankLine(mode, state) { + if (mode.blankLine) { return mode.blankLine(state) } + if (!mode.innerMode) { return } + var inner = innerMode(mode, state); + if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) { inner[0] = innerMode(mode, state).mode; } + var style = mode.token(stream, state); + if (stream.pos > stream.start) { return style } + } + throw new Error("Mode " + mode.name + " failed to advance stream.") + } + + var Token = function(stream, type, state) { + this.start = stream.start; this.end = stream.pos; + this.string = stream.current(); + this.type = type || null; + this.state = state; + }; + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize, context), tokens; + if (asArray) { tokens = []; } + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, context.state); + if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))); } + } + return asArray ? tokens : new Token(stream, style, context.state) + } + + function extractLineClasses(type, output) { + if (type) { for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) { break } + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + { output[prop] = lineClass[2]; } + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + { output[prop] += " " + lineClass[2]; } + } } + return type + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans; } + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize, context), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses); } + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) { processLine(cm, text, context, stream.pos); } + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) { style = "m-" + (style ? mName + " " + style : mName); } + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 5000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 + // characters, and returns inaccurate measurements in nodes + // starting around 5000 chars. + var pos = Math.min(stream.pos, curStart + 5000); + f(pos, curStyle); + curStart = pos; + } + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) { return doc.first } + var line = getLine(doc, search - 1), after = line.stateAfter; + if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) + { return search } + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline + } + + function retreatFrontier(doc, n) { + doc.modeFrontier = Math.min(doc.modeFrontier, n); + if (doc.highlightFrontier < n - 10) { return } + var start = doc.first; + for (var line = n - 1; line > start; line--) { + var saved = getLine(doc, line).stateAfter; + // change is on 3 + // state on line 1 looked ahead 2 -- so saw 3 + // test 1 + 2 < 3 should cover this + if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { + start = line + 1; + break + } + } + doc.highlightFrontier = Math.min(doc.highlightFrontier, start); + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + + Line.prototype.lineNo = function () { return lineNo(this) }; + eventMixin(Line); + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + if (line.order != null) { line.order = null; } + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) { return null } + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")) + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, + col: 0, pos: 0, cm: cm, + trailingSpace: false, + splitSpaces: cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0); + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) + { builder.addToken = buildTokenBadBidi(builder.addToken, order); } + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); } + if (line.styleClasses.textClass) + { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); } + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); } + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) + ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit) { + var last = builder.content.lastChild; + if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) + { builder.content.className = "cm-tab-wrap-hack"; } + } + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + { builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); } + + return builder + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) { return } + var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + var content; + if (!special.test(text)) { + builder.col += text.length; + content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) { mustWrap = true; } + builder.pos += text.length; + } else { + content = document.createDocumentFragment(); + var pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])); } + else { content.appendChild(txt); } + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) { break } + pos += skipped + 1; + var txt$1 = (void 0); + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt$1.setAttribute("role", "presentation"); + txt$1.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else if (m[0] == "\r" || m[0] == "\n") { + txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); + txt$1.setAttribute("cm-text", m[0]); + builder.col += 1; + } else { + txt$1 = builder.cm.options.specialCharPlaceholder(m[0]); + txt$1.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])); } + else { content.appendChild(txt$1); } + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt$1); + builder.pos++; + } + } + builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) { fullStyle += startStyle; } + if (endStyle) { fullStyle += endStyle; } + var token = elt("span", [content], fullStyle, css); + if (title) { token.title = title; } + return builder.content.appendChild(token) + } + builder.content.appendChild(content); + } + + // Change some spaces to NBSP to prevent the browser from collapsing + // trailing spaces at the end of a line when rendering text (issue #1362). + function splitSpaces(text, trailingBefore) { + if (text.length > 1 && !/ /.test(text)) { return text } + var spaceBefore = trailingBefore, result = ""; + for (var i = 0; i < text.length; i++) { + var ch = text.charAt(i); + if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) + { ch = "\u00a0"; } + result += ch; + spaceBefore = ch == " "; + } + return result + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function (builder, text, style, startStyle, endStyle, title, css) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + var part = (void 0); + for (var i = 0; i < order.length; i++) { + part = order[i]; + if (part.to > start && part.from <= start) { break } + } + if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, title, css) } + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + } + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); } + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + { widget = builder.content.appendChild(document.createElement("span")); } + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + builder.trailingSpace = false; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i$1 = 1; i$1 < styles.length; i$1+=2) + { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)); } + return + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = [], endStyles = (void 0); + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { + foundBookmarks.push(m); + } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { + if (sp.to != null && sp.to != pos && nextChange > sp.to) { + nextChange = sp.to; + spanEndStyle = ""; + } + if (m.className) { spanStyle += " " + m.className; } + if (m.css) { css = (css ? css + ";" : "") + m.css; } + if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle; } + if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to); } + if (m.title && !title) { title = m.title; } + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + { collapsed = sp; } + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + } + if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) + { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1]; } } } + + if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) + { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]); } } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) { return } + if (collapsed.to == pos) { collapsed = false; } + } + } + if (pos >= len) { break } + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array + } + + var operationGroup = null; + + function pushOperation(op) { + if (operationGroup) { + operationGroup.ops.push(op); + } else { + op.ownsGroup = operationGroup = { + ops: [op], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + { callbacks[i].call(null); } + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } + } + } while (i < callbacks.length) + } + + function finishOperation(op, endCb) { + var group = op.ownsGroup; + if (!group) { return } + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + endCb(group); + } + } + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = getHandlers(emitter, type); + if (!arr.length) { return } + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + var loop = function ( i ) { + list.push(function () { return arr[i].apply(null, args); }); + }; + + for (var i = 0; i < arr.length; ++i) + loop( i ); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) { delayed[i](); } + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") { updateLineText(cm, lineView); } + else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims); } + else if (type == "class") { updateLineClasses(cm, lineView); } + else if (type == "widget") { updateLineWidgets(cm, lineView, dims); } + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + { lineView.text.parentNode.replaceChild(lineView.node, lineView.text); } + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) { lineView.node.style.zIndex = 2; } + } + return lineView.node + } + + function updateLineBackground(cm, lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) { cls += " CodeMirror-linebackground"; } + if (lineView.background) { + if (cls) { lineView.background.className = cls; } + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + cm.display.input.setUneditable(lineView.background); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built + } + return buildLineContent(cm, lineView) + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) { lineView.node = built.pre; } + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(cm, lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(cm, lineView) { + updateLineBackground(cm, lineView); + if (lineView.line.wrapClass) + { ensureLineWrapped(lineView).className = lineView.line.wrapClass; } + else if (lineView.node != lineView.text) + { lineView.node.className = ""; } + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + if (lineView.gutterBackground) { + lineView.node.removeChild(lineView.gutterBackground); + lineView.gutterBackground = null; + } + if (lineView.line.gutterClass) { + var wrap = ensureLineWrapped(lineView); + lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, + ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(lineView.gutterBackground); + wrap.insertBefore(lineView.gutterBackground, lineView.text); + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap$1 = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(gutterWrap); + wrap$1.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + { gutterWrap.className += " " + lineView.line.gutterClass; } + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + { lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))); } + if (markers) { for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))); } + } } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) { lineView.alignable = null; } + for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { + next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + { lineView.node.removeChild(node); } + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) { lineView.bgClass = built.bgClass; } + if (built.textClass) { lineView.textClass = built.textClass; } + + updateLineClasses(cm, lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); } } + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) { return } + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true"); } + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + { wrap.insertBefore(node, lineView.gutter || lineView.text); } + else + { wrap.appendChild(node); } + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px"; } + } + } + + function widgetHeight(widget) { + if (widget.height != null) { return widget.height } + var cm = widget.doc.cm; + if (!cm) { return 0 } + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; } + if (widget.noHScroll) + { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; } + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.parentNode.offsetHeight + } + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + { return true } + } + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} + function paddingH(display) { + if (display.cachedPaddingH) { return display.cachedPaddingH } + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } + return data + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + { heights.push((cur.bottom + next.top) / 2 - rect.top); } + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + { return {map: lineView.measure.map, cache: lineView.measure.cache} } + for (var i = 0; i < lineView.rest.length; i++) + { if (lineView.rest[i] == line) + { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } + for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) + { if (lineNo(lineView.rest[i$1]) > lineN) + { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + { return cm.display.view[findViewIndex(cm, lineN)] } + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + { return ext } + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) { + view = null; + } else if (view && view.changes) { + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + cm.curOp.forceUpdate = true; + } + if (!view) + { view = updateExternalMeasurement(cm, line); } + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + } + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) { ch = -1; } + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + { prepared.rect = prepared.view.text.getBoundingClientRect(); } + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) { prepared.cache[key] = found; } + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom} + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map$$1, ch, bias) { + var node, start, end, collapse, mStart, mEnd; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map$$1.length; i += 3) { + mStart = map$$1[i]; + mEnd = map$$1[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map$$1.length - 3 || ch == mEnd && map$$1[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) { collapse = "right"; } + } + if (start != null) { + node = map$$1[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + { collapse = bias; } + if (bias == "left" && start == 0) + { while (i && map$$1[i - 2] == map$$1[i - 3] && map$$1[i - 1].insertLeft) { + node = map$$1[(i -= 3) + 2]; + collapse = "left"; + } } + if (bias == "right" && start == mEnd - mStart) + { while (i < map$$1.length - 3 && map$$1[i + 3] == map$$1[i + 4] && !map$$1[i + 5].insertLeft) { + node = map$$1[(i += 3) + 2]; + collapse = "right"; + } } + break + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} + } + + function getUsefulRect(rects, bias) { + var rect = nullRect; + if (bias == "left") { for (var i = 0; i < rects.length; i++) { + if ((rect = rects[i]).left != rect.right) { break } + } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { + if ((rect = rects[i$1]).left != rect.right) { break } + } } + return rect + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start; } + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end; } + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) + { rect = node.parentNode.getBoundingClientRect(); } + else + { rect = getUsefulRect(range(node, start, end).getClientRects(), bias); } + if (rect.left || rect.right || start == 0) { break } + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect); } + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) { collapse = bias = "right"; } + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + { rect = rects[bias == "right" ? rects.length - 1 : 0]; } + else + { rect = node.getBoundingClientRect(); } + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; } + else + { rect = nullRect; } + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + var i = 0; + for (; i < heights.length - 1; i++) + { if (mid < heights[i]) { break } } + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) { result.bogus = true; } + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + { return rect } + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY} + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { lineView.measure.caches[i] = {}; } } + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + { clearLineMeasurementCacheFor(cm.display.view[i]); } + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true; } + cm.display.lineNumChars = null; + } + + function pageScrollX() { + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 + // which causes page_Offset and bounding client rects to use + // different reference viewports and invalidate our calculations. + if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } + return window.pageXOffset || (document.documentElement || document.body).scrollLeft + } + function pageScrollY() { + if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } + return window.pageYOffset || (document.documentElement || document.body).scrollTop + } + + function widgetTopHeight(lineObj) { + var height = 0; + if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) + { height += widgetHeight(lineObj.widgets[i]); } } } + return height + } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"./null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { + if (!includeWidgets) { + var height = widgetTopHeight(lineObj); + rect.top += height; rect.bottom += height; + } + if (context == "line") { return rect } + if (!context) { context = "local"; } + var yOff = heightAtLine(lineObj); + if (context == "local") { yOff += paddingTop(cm.display); } + else { yOff -= cm.display.viewOffset; } + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"./null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") { return coords } + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) { lineObj = getLine(cm.doc, pos.line); } + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` + // and after `char - 1` in writing order of `char - 1` + // A cursor Pos(line, char, "after") is on the same visual line as `char` + // and before `char` in writing order of `char` + // Examples (upper-case letters are RTL, lower-case are LTR): + // Pos(0, 1, ...) + // before after + // ab a|b a|b + // aB a|B aB| + // Ab |Ab A|b + // AB B|A B|A + // Every position after the last character on a line is considered to stick + // to the last character on the line. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) { m.left = m.right; } else { m.right = m.left; } + return intoCoordSystem(cm, lineObj, m, context) + } + var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky; + if (ch >= lineObj.text.length) { + ch = lineObj.text.length; + sticky = "before"; + } else if (ch <= 0) { + ch = 0; + sticky = "after"; + } + if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } + + function getBidi(ch, partPos, invert) { + var part = order[partPos], right = part.level == 1; + return get(invert ? ch - 1 : ch, right != invert) + } + var partPos = getBidiPartAt(order, ch, sticky); + var other = bidiOther; + var val = getBidi(ch, partPos, sticky == "before"); + if (other != null) { val.other = getBidi(ch, other, sticky != "before"); } + return val + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0; + pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch; } + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height} + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, sticky, outside, xRel) { + var pos = Pos(line, ch, sticky); + pos.xRel = xRel; + if (outside) { pos.outside = true; } + return pos + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) { return PosWithInfo(doc.first, 0, null, true, -1) } + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) } + if (x < 0) { x = 0; } + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0)); + if (!collapsed) { return found } + var rangeEnd = collapsed.find(1); + if (rangeEnd.line == lineN) { return rangeEnd } + lineObj = getLine(doc, lineN = rangeEnd.line); + } + } + + function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { + y -= widgetTopHeight(lineObj); + var end = lineObj.text.length; + var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0); + end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end); + return {begin: begin, end: end} + } + + function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top; + return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) + } + + // Returns true if the given side of a box is after the given + // coordinates, in top-to-bottom, left-to-right order. + function boxIsAfter(box, x, y, left) { + return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x + } + + function coordsCharInner(cm, lineObj, lineNo$$1, x, y) { + // Move y into line-local coordinate space + y -= heightAtLine(lineObj); + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + // When directly calling `measureCharPrepared`, we have to adjust + // for the widgets at this line. + var widgetHeight$$1 = widgetTopHeight(lineObj); + var begin = 0, end = lineObj.text.length, ltr = true; + + var order = getOrder(lineObj, cm.doc.direction); + // If the line isn't plain left-to-right text, first figure out + // which bidi section the coordinates fall into. + if (order) { + var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) + (cm, lineObj, lineNo$$1, preparedMeasure, order, x, y); + ltr = part.level != 1; + // The awkward -1 offsets are needed because findFirst (called + // on these below) will treat its first bound as inclusive, + // second as exclusive, but we want to actually address the + // characters in the part's range + begin = ltr ? part.from : part.to - 1; + end = ltr ? part.to : part.from - 1; + } + + // A binary search to find the first character whose bounding box + // starts after the coordinates. If we run across any whose box wrap + // the coordinates, store that. + var chAround = null, boxAround = null; + var ch = findFirst(function (ch) { + var box = measureCharPrepared(cm, preparedMeasure, ch); + box.top += widgetHeight$$1; box.bottom += widgetHeight$$1; + if (!boxIsAfter(box, x, y, false)) { return false } + if (box.top <= y && box.left <= x) { + chAround = ch; + boxAround = box; + } + return true + }, begin, end); + + var baseX, sticky, outside = false; + // If a box around the coordinates was found, use that + if (boxAround) { + // Distinguish coordinates nearer to the left or right side of the box + var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr; + ch = chAround + (atStart ? 0 : 1); + sticky = atStart ? "after" : "before"; + baseX = atLeft ? boxAround.left : boxAround.right; + } else { + // (Adjust for extended bound, if necessary.) + if (!ltr && (ch == end || ch == begin)) { ch++; } + // To determine which side to associate with, get the box to the + // left of the character and compare it's vertical position to the + // coordinates + sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : + (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight$$1 <= y) == ltr ? + "after" : "before"; + // Now get accurate coordinates for this place, in order to get a + // base X position + var coords = cursorCoords(cm, Pos(lineNo$$1, ch, sticky), "line", lineObj, preparedMeasure); + baseX = coords.left; + outside = y < coords.top || y >= coords.bottom; + } + + ch = skipExtendingChars(lineObj.text, ch, 1); + return PosWithInfo(lineNo$$1, ch, sticky, outside, x - baseX) + } + + function coordsBidiPart(cm, lineObj, lineNo$$1, preparedMeasure, order, x, y) { + // Bidi parts are sorted left-to-right, and in a non-line-wrapping + // situation, we can take this ordering to correspond to the visual + // ordering. This finds the first part whose end is after the given + // coordinates. + var index = findFirst(function (i) { + var part = order[i], ltr = part.level != 1; + return boxIsAfter(cursorCoords(cm, Pos(lineNo$$1, ltr ? part.to : part.from, ltr ? "before" : "after"), + "line", lineObj, preparedMeasure), x, y, true) + }, 0, order.length - 1); + var part = order[index]; + // If this isn't the first part, the part's start is also after + // the coordinates, and the coordinates aren't on the same line as + // that start, move one part back. + if (index > 0) { + var ltr = part.level != 1; + var start = cursorCoords(cm, Pos(lineNo$$1, ltr ? part.from : part.to, ltr ? "after" : "before"), + "line", lineObj, preparedMeasure); + if (boxIsAfter(start, x, y, true) && start.top > y) + { part = order[index - 1]; } + } + return part + } + + function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { + // In a wrapped line, rtl text on wrapping boundaries can do things + // that don't correspond to the ordering in our `order` array at + // all, so a binary search doesn't work, and we want to return a + // part that only spans one line so that the binary search in + // coordsCharInner is safe. As such, we first find the extent of the + // wrapped line, and then do a flat search in which we discard any + // spans that aren't on the line. + var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); + var begin = ref.begin; + var end = ref.end; + if (/\s/.test(lineObj.text.charAt(end - 1))) { end--; } + var part = null, closestDist = null; + for (var i = 0; i < order.length; i++) { + var p = order[i]; + if (p.from >= end || p.to <= begin) { continue } + var ltr = p.level != 1; + var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right; + // Weigh against spans ending before this, so that they are only + // picked if nothing ends after + var dist = endX < x ? x - endX + 1e9 : endX - x; + if (!part || closestDist > dist) { + part = p; + closestDist = dist; + } + } + if (!part) { part = order[order.length - 1]; } + // Clip the part to the wrapped line. + if (part.from < begin) { part = {from: begin, to: part.to, level: part.level}; } + if (part.to > end) { part = {from: part.from, to: end, level: part.level}; } + return part + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) { return display.cachedTextHeight } + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) { display.cachedTextHeight = height; } + removeChildren(display.measure); + return height || 1 + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) { return display.cachedCharWidth } + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) { display.cachedCharWidth = width; } + return width || 10 + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth} + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function (line) { + if (lineIsHidden(cm.doc, line)) { return 0 } + + var widgetsHeight = 0; + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height; } + } } + + if (wrapping) + { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } + else + { return widgetsHeight + th } + } + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function (line) { + var estHeight = est(line); + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + }); + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) { return null } + n -= cm.display.viewFrom; + if (n < 0) { return null } + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) { return i } + } + } + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + if ( primary === void 0 ) primary = true; + + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (!primary && i == doc.sel.primIndex) { continue } + var range$$1 = doc.sel.ranges[i]; + if (range$$1.from().line >= cm.display.viewTo || range$$1.to().line < cm.display.viewFrom) { continue } + var collapsed = range$$1.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + { drawSelectionCursor(cm, range$$1.head, curFragment); } + if (!collapsed) + { drawSelectionRange(cm, range$$1, selFragment); } + } + return result + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, head, output) { + var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range$$1, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + var docLTR = doc.direction == "ltr"; + + function add(left, top, width, bottom) { + if (top < 0) { top = 0; } + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias) + } + + function wrapX(pos, dir, side) { + var extent = wrappedLineExtentChar(cm, lineObj, null, pos); + var prop = (dir == "ltr") == (side == "after") ? "left" : "right"; + var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1); + return coords(ch, prop)[prop] + } + + var order = getOrder(lineObj, doc.direction); + iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { + var ltr = dir == "ltr"; + var fromPos = coords(from, ltr ? "left" : "right"); + var toPos = coords(to - 1, ltr ? "right" : "left"); + + var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen; + var first = i == 0, last = !order || i == order.length - 1; + if (toPos.top - fromPos.top <= 3) { // Single line + var openLeft = (docLTR ? openStart : openEnd) && first; + var openRight = (docLTR ? openEnd : openStart) && last; + var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left; + var right = openRight ? rightSide : (ltr ? toPos : fromPos).right; + add(left, fromPos.top, right - left, fromPos.bottom); + } else { // Multiple lines + var topLeft, topRight, botLeft, botRight; + if (ltr) { + topLeft = docLTR && openStart && first ? leftSide : fromPos.left; + topRight = docLTR ? rightSide : wrapX(from, dir, "before"); + botLeft = docLTR ? leftSide : wrapX(to, dir, "after"); + botRight = docLTR && openEnd && last ? rightSide : toPos.right; + } else { + topLeft = !docLTR ? leftSide : wrapX(from, dir, "before"); + topRight = !docLTR && openStart && first ? rightSide : fromPos.right; + botLeft = !docLTR && openEnd && last ? leftSide : toPos.left; + botRight = !docLTR ? rightSide : wrapX(to, dir, "after"); + } + add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom); + if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top); } + add(botLeft, toPos.top, botRight - botLeft, toPos.bottom); + } + + if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos; } + if (cmpCoords(toPos, start) < 0) { start = toPos; } + if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos; } + if (cmpCoords(toPos, end) < 0) { end = toPos; } + }); + return {start: start, end: end} + } + + var sFrom = range$$1.from(), sTo = range$$1.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + { add(leftSide, leftEnd.bottom, null, rightStart.top); } + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) { return } + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + { display.blinker = setInterval(function () { return display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; }, + cm.options.cursorBlinkRate); } + else if (cm.options.cursorBlinkRate < 0) + { display.cursorDiv.style.visibility = "hidden"; } + } + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function () { if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } }, 100); + } + + function onFocus(cm, e) { + if (cm.state.delayingBlurEvent) { cm.state.delayingBlurEvent = false; } + + if (cm.options.readOnly == "nocursor") { return } + if (!cm.state.focused) { + signal(cm, "focus", cm, e); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20); } // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm, e) { + if (cm.state.delayingBlurEvent) { return } + + if (cm.state.focused) { + signal(cm, "blur", cm, e); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false; } }, 150); + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height = (void 0); + if (cur.hidden) { continue } + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) { height = textHeight(display); } + if (diff > .005 || diff < -.005) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) + { updateWidgetHeight(cur.rest[j]); } } + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i], parent = w.node.parentNode; + if (parent) { w.height = parent.offsetHeight; } + } } + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)} + } + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { + if (cm.options.fixedGutter) { + if (view[i].gutter) + { view[i].gutter.style.left = left; } + if (view[i].gutterBackground) + { view[i].gutterBackground.style.left = left; } + } + var align = view[i].alignable; + if (align) { for (var j = 0; j < align.length; j++) + { align[j].style.left = left; } } + } } + if (cm.options.fixedGutter) + { display.gutters.style.left = (comp + gutterW) + "px"; } + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) { return false } + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true + } + return false + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, rect) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (rect.top + box.top < 0) { doScroll = true; } + else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false; } + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) { margin = 0; } + var rect; + if (!cm.options.lineWrapping && pos == end) { + // Set pos and end to the cursor positions around the character pos sticks to + // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch + // If pos == Pos(_, 0, "before"), pos and end are unchanged + pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos; + end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos; + } + for (var limit = 0; limit < 5; limit++) { + var changed = false; + var coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + rect = {left: Math.min(coords.left, endCoords.left), + top: Math.min(coords.top, endCoords.top) - margin, + right: Math.max(coords.left, endCoords.left), + bottom: Math.max(coords.bottom, endCoords.bottom) + margin}; + var scrollPos = calculateScrollPos(cm, rect); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + updateScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true; } + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true; } + } + if (!changed) { break } + } + return rect + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, rect) { + var scrollPos = calculateScrollPos(cm, rect); + if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop); } + if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); } + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, rect) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (rect.top < 0) { rect.top = 0; } + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen; } + var docBottom = cm.doc.height + paddingVert(display); + var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin; + if (rect.top < screentop) { + result.scrollTop = atTop ? 0 : rect.top; + } else if (rect.bottom > screentop + screen) { + var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen); + if (newTop != screentop) { result.scrollTop = newTop; } + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = rect.right - rect.left > screenw; + if (tooWide) { rect.right = rect.left + screenw; } + if (rect.left < 10) + { result.scrollLeft = 0; } + else if (rect.left < screenleft) + { result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)); } + else if (rect.right > screenw + screenleft - 3) + { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw; } + return result + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollTop(cm, top) { + if (top == null) { return } + resolveScrollToPos(cm); + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(); + cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin}; + } + + function scrollToCoords(cm, x, y) { + if (x != null || y != null) { resolveScrollToPos(cm); } + if (x != null) { cm.curOp.scrollLeft = x; } + if (y != null) { cm.curOp.scrollTop = y; } + } + + function scrollToRange(cm, range$$1) { + resolveScrollToPos(cm); + cm.curOp.scrollToPos = range$$1; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range$$1 = cm.curOp.scrollToPos; + if (range$$1) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range$$1.from), to = estimateCoords(cm, range$$1.to); + scrollToCoordsRange(cm, from, to, range$$1.margin); + } + } + + function scrollToCoordsRange(cm, from, to, margin) { + var sPos = calculateScrollPos(cm, { + left: Math.min(from.left, to.left), + top: Math.min(from.top, to.top) - margin, + right: Math.max(from.right, to.right), + bottom: Math.max(from.bottom, to.bottom) + margin + }); + scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop); + } + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function updateScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) { return } + if (!gecko) { updateDisplaySimple(cm, {top: val}); } + setScrollTop(cm, val, true); + if (gecko) { updateDisplaySimple(cm); } + startWorker(cm, 100); + } + + function setScrollTop(cm, val, forceScroll) { + val = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val); + if (cm.display.scroller.scrollTop == val && !forceScroll) { return } + cm.doc.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val; } + } + + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller, forceScroll) { + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val; } + cm.display.scrollbars.setScrollLeft(val); + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + } + } + + var NativeScrollbars = function(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + vert.tabIndex = horiz.tabIndex = -1; + place(vert); place(horiz); + + on(vert, "scroll", function () { + if (vert.clientHeight) { scroll(vert.scrollTop, "vertical"); } + }); + on(horiz, "scroll", function () { + if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal"); } + }); + + this.checkedZeroWidth = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } + }; + + NativeScrollbars.prototype.update = function (measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedZeroWidth && measure.clientHeight > 0) { + if (sWidth == 0) { this.zeroWidthHack(); } + this.checkedZeroWidth = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} + }; + + NativeScrollbars.prototype.setScrollLeft = function (pos) { + if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos; } + if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz"); } + }; + + NativeScrollbars.prototype.setScrollTop = function (pos) { + if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos; } + if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert"); } + }; + + NativeScrollbars.prototype.zeroWidthHack = function () { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.height = this.vert.style.width = w; + this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; + this.disableHoriz = new Delayed; + this.disableVert = new Delayed; + }; + + NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { + bar.style.pointerEvents = "auto"; + function maybeDisable() { + // To find out whether the scrollbar is still visible, we + // check whether the element under the pixel in the bottom + // right corner of the scrollbar box is the scrollbar box + // itself (when the bar is still visible) or its filler child + // (when the bar is hidden). If it is still visible, we keep + // it enabled, if it's hidden, we disable pointer events. + var box = bar.getBoundingClientRect(); + var elt$$1 = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) + : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1); + if (elt$$1 != bar) { bar.style.pointerEvents = "none"; } + else { delay.set(1000, maybeDisable); } + } + delay.set(1000, maybeDisable); + }; + + NativeScrollbars.prototype.clear = function () { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + }; + + var NullScrollbars = function () {}; + + NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; + NullScrollbars.prototype.setScrollLeft = function () {}; + NullScrollbars.prototype.setScrollTop = function () {}; + NullScrollbars.prototype.clear = function () {}; + + function updateScrollbars(cm, measure) { + if (!measure) { measure = measureForScrollbars(cm); } + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + { updateHeightsInViewport(cm); } + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else { d.scrollbarFiller.style.display = ""; } + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else { d.gutterFiller.style.display = ""; } + } + + var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function () { + if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0); } + }); + node.setAttribute("cm-not-content", "true"); + }, function (pos, axis) { + if (axis == "horizontal") { setScrollLeft(cm, pos); } + else { updateScrollTop(cm, pos); } + }, cm); + if (cm.display.scrollbars.addClass) + { addClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + pushOperation(cm.curOp); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp; + if (op) { finishOperation(op, function (group) { + for (var i = 0; i < group.ops.length; i++) + { group.ops[i].cm.curOp = null; } + endOperations(group); + }); } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + { endOperation_R1(ops[i]); } + for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) + { endOperation_W1(ops[i$1]); } + for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM + { endOperation_R2(ops[i$2]); } + for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) + { endOperation_W2(ops[i$3]); } + for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM + { endOperation_finish(ops[i$4]); } + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) { findMaxLine(cm); } + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) { updateHeightsInViewport(cm); } + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + { op.preparedSelection = display.input.prepareSelection(); } + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); } + cm.display.maxLineChanged = false; + } + + var takeFocus = op.focus && op.focus == activeElt(); + if (op.preparedSelection) + { cm.display.input.showSelection(op.preparedSelection, takeFocus); } + if (op.updatedDisplay || op.startHeight != cm.doc.height) + { updateScrollbars(cm, op.barMeasure); } + if (op.updatedDisplay) + { setDocumentHeight(cm, op.barMeasure); } + + if (op.selectionChanged) { restartBlink(cm); } + + if (cm.state.focused && op.updateInput) + { cm.display.input.reset(op.typing); } + if (takeFocus) { ensureFocus(op.cm); } + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) { postUpdateDisplay(cm, op.update); } + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + { display.wheelStartX = display.wheelStartY = null; } + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll); } + + if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true); } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + maybeScrollWindow(cm, rect); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) { for (var i = 0; i < hidden.length; ++i) + { if (!hidden[i].lines.length) { signal(hidden[i], "hide"); } } } + if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) + { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide"); } } } + + if (display.wrapper.offsetHeight) + { doc.scrollTop = cm.display.scroller.scrollTop; } + + // Fire change events, and delayed event handlers + if (op.changeObjs) + { signal(cm, "changes", cm, op.changeObjs); } + if (op.update) + { op.update.finish(); } + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) { return f() } + startOperation(cm); + try { return f() } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) { return f.apply(cm, arguments) } + startOperation(cm); + try { return f.apply(cm, arguments) } + finally { endOperation(cm); } + } + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) { return f.apply(this, arguments) } + startOperation(this); + try { return f.apply(this, arguments) } + finally { endOperation(this); } + } + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) { return f.apply(this, arguments) } + startOperation(cm); + try { return f.apply(this, arguments) } + finally { endOperation(cm); } + } + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) { from = cm.doc.first; } + if (to == null) { to = cm.doc.first + cm.doc.size; } + if (!lendiff) { lendiff = 0; } + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + { display.updateLineNumbers = from; } + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + { resetView(cm); } + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut$1 = viewCuttingPoint(cm, from, from, -1); + if (cut$1) { + display.view = display.view.slice(0, cut$1.index); + display.viewTo = cut$1.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + { ext.lineN += lendiff; } + else if (from < ext.lineN + ext.size) + { display.externalMeasured = null; } + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + { display.externalMeasured = null; } + + if (line < display.viewFrom || line >= display.viewTo) { return } + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) { return } + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) { arr.push(type); } + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + { return {index: index, lineN: newN} } + var n = cm.display.viewFrom; + for (var i = 0; i < index; i++) + { n += view[i].size; } + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) { return null } + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) { return null } + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN} + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); } + else if (display.viewFrom < from) + { display.view = display.view.slice(findViewIndex(cm, from)); } + display.viewFrom = from; + if (display.viewTo < to) + { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); } + else if (display.viewTo > to) + { display.view = display.view.slice(0, findViewIndex(cm, to)); } + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty; } + } + return dirty + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.highlightFrontier < cm.display.viewTo) + { cm.state.highlight.set(time, bind(highlightWorker, cm)); } + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.highlightFrontier >= cm.display.viewTo) { return } + var end = +new Date + cm.options.workTime; + var context = getContextBefore(cm, doc.highlightFrontier); + var changedLines = []; + + doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { + if (context.line >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null; + var highlighted = highlightLine(cm, line, context, true); + if (resetState) { context.state = resetState; } + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) { line.styleClasses = newCls; } + else if (oldCls) { line.styleClasses = null; } + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i]; } + if (ischange) { changedLines.push(context.line); } + line.stateAfter = context.save(); + context.nextLine(); + } else { + if (line.text.length <= cm.options.maxHighlightLength) + { processLine(cm, line.text, context); } + line.stateAfter = context.line % 5 == 0 ? context.save() : null; + context.nextLine(); + } + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true + } + }); + doc.highlightFrontier = context.line; + doc.modeFrontier = Math.max(doc.modeFrontier, context.line); + if (changedLines.length) { runInOp(cm, function () { + for (var i = 0; i < changedLines.length; i++) + { regLineChange(cm, changedLines[i], "text"); } + }); } + } + + // DISPLAY DRAWING + + var DisplayUpdate = function(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + }; + + DisplayUpdate.prototype.signal = function (emitter, type) { + if (hasHandler(emitter, type)) + { this.events.push(arguments); } + }; + DisplayUpdate.prototype.finish = function () { + var this$1 = this; + + for (var i = 0; i < this.events.length; i++) + { signal.apply(null, this$1.events[i]); } + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + function selectionSnapshot(cm) { + if (cm.hasFocus()) { return null } + var active = activeElt(); + if (!active || !contains(cm.display.lineDiv, active)) { return null } + var result = {activeElt: active}; + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { + result.anchorNode = sel.anchorNode; + result.anchorOffset = sel.anchorOffset; + result.focusNode = sel.focusNode; + result.focusOffset = sel.focusOffset; + } + } + return result + } + + function restoreSelection(snapshot) { + if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } + snapshot.activeElt.focus(); + if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { + var sel = window.getSelection(), range$$1 = document.createRange(); + range$$1.setEnd(snapshot.anchorNode, snapshot.anchorOffset); + range$$1.collapse(false); + sel.removeAllRanges(); + sel.addRange(range$$1); + sel.extend(snapshot.focusNode, snapshot.focusOffset); + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + { return false } + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom); } + if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo); } + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + { return false } + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var selSnapshot = selectionSnapshot(cm); + if (toUpdate > 4) { display.lineDiv.style.display = "none"; } + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) { display.lineDiv.style.display = ""; } + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + restoreSelection(selSnapshot); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = display.sizer.style.minHeight = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true + } + + function postUpdateDisplay(cm, update) { + var viewport = update.viewport; + + for (var first = true;; first = false) { + if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; } + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + { break } + } + if (!updateDisplayIfNeeded(cm, update)) { break } + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.force = false; + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.finish(); + } + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + { node.style.display = "none"; } + else + { node.parentNode.removeChild(node); } + return next + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) ; else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) { cur = rm(cur); } + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false; } + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) { cur = rm(cur); } + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + var i = 0; + for (; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) { wheelPixelsPerUnit = -.53; } + else if (gecko) { wheelPixelsPerUnit = 15; } + else if (chrome) { wheelPixelsPerUnit = -.7; } + else if (safari) { wheelPixelsPerUnit = -1/3; } + + function wheelEventDelta(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail; } + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail; } + else if (dy == null) { dy = e.wheelDelta; } + return {x: dx, y: dy} + } + function wheelEventPixels(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta + } + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + var canScrollX = scroll.scrollWidth > scroll.clientWidth; + var canScrollY = scroll.scrollHeight > scroll.clientHeight; + if (!(dx && canScrollX || dy && canScrollY)) { return } + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy && canScrollY) + { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)); } + setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)); + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + { e_preventDefault(e); } + display.wheelStartX = null; // Abort measurement, if in progress + return + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) { top = Math.max(0, top + pixels - 50); } + else { bot = Math.min(cm.doc.height, bot + pixels + 50); } + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function () { + if (display.wheelStartX == null) { return } + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) { return } + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + var Selection = function(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + }; + + Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; + + Selection.prototype.equals = function (other) { + var this$1 = this; + + if (other == this) { return true } + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } + for (var i = 0; i < this.ranges.length; i++) { + var here = this$1.ranges[i], there = other.ranges[i]; + if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } + } + return true + }; + + Selection.prototype.deepCopy = function () { + var this$1 = this; + + var out = []; + for (var i = 0; i < this.ranges.length; i++) + { out[i] = new Range(copyPos(this$1.ranges[i].anchor), copyPos(this$1.ranges[i].head)); } + return new Selection(out, this.primIndex) + }; + + Selection.prototype.somethingSelected = function () { + var this$1 = this; + + for (var i = 0; i < this.ranges.length; i++) + { if (!this$1.ranges[i].empty()) { return true } } + return false + }; + + Selection.prototype.contains = function (pos, end) { + var this$1 = this; + + if (!end) { end = pos; } + for (var i = 0; i < this.ranges.length; i++) { + var range = this$1.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + { return i } + } + return -1 + }; + + var Range = function(anchor, head) { + this.anchor = anchor; this.head = head; + }; + + Range.prototype.from = function () { return minPos(this.anchor, this.head) }; + Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; + Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(cm, ranges, primIndex) { + var mayTouch = cm && cm.options.selectionsMayTouch; + var prim = ranges[primIndex]; + ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + var diff = cmp(prev.to(), cur.from()); + if (mayTouch && !cur.empty() ? diff > 0 : diff >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) { --primIndex; } + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex) + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0) + } + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + function changeEnd(change) { + if (!change.text) { return change.to } + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) + } + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) { return pos } + if (cmp(pos, change.to) <= 0) { return changeEnd(change) } + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch; } + return Pos(line, ch) + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(doc.cm, out, doc.sel.primIndex) + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + { return Pos(nw.line, pos.ch - old.ch + nw.ch) } + else + { return Pos(nw.line + (pos.line - old.line), pos.ch) } + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex) + } + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function (line) { + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + }); + cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) { regChange(cm); } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore) + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight$$1) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight$$1); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + var result = []; + for (var i = start; i < end; ++i) + { result.push(new Line(text[i], spansFor(i), estimateHeight$$1)); } + return result + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) { doc.remove(from.line, nlines); } + if (added.length) { doc.insert(from.line, added); } + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added$1 = linesFor(1, text.length - 1); + added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight$$1)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added$1); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added$2 = linesFor(1, text.length - 1); + if (nlines > 1) { doc.remove(from.line + 1, nlines - 1); } + doc.insert(from.line + 1, added$2); + } + + signalLater(doc, "change", doc, change); + } + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) { continue } + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) { continue } + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) { throw new Error("This document is already in use.") } + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + setDirectionClass(cm); + if (!cm.options.lineWrapping) { findMaxLine(cm); } + cm.options.mode = doc.modeOption; + regChange(cm); + } + + function setDirectionClass(cm) { + (cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl"); + } + + function directionChanged(cm) { + runInOp(cm, function () { + setDirectionClass(cm); + regChange(cm); + }); + } + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true); + return histChange + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) { array.pop(); } + else { break } + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done) + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done) + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done) + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, or are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + var last; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + { pushSelectionToHistory(doc.sel, hist.done); } + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) { hist.done.shift(); } + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) { signal(doc, "historyAdded"); } + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + { hist.done[hist.done.length - 1] = sel; } + else + { pushSelectionToHistory(sel, hist.done); } + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + { clearSelectionEvents(hist.undone); } + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + { dest.push(sel); } + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { + if (line.markedSpans) + { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; } + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) { return null } + var out; + for (var i = 0; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i); } } + else if (out) { out.push(spans[i]); } + } + return !out ? spans : out.length ? out : null + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) { return null } + var nw = []; + for (var i = 0; i < change.text.length; ++i) + { nw.push(removeClearedSpans(found[i])); } + return nw + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) { return stretched } + if (!stretched) { return old } + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + { if (oldCur[k].marker == span.marker) { continue spans } } + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + var copy = []; + for (var i = 0; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m = (void 0); + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } } } + } + } + return copy + } + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(range, head, other, extend) { + if (extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head) + } else { + return new Range(other || head, head) + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options, extend) { + if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend); } + setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + var out = []; + var extend = doc.cm && (doc.cm.display.shift || doc.extend); + for (var i = 0; i < doc.sel.ranges.length; i++) + { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend); } + var newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel, options) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + var this$1 = this; + + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + { this$1.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); } + }, + origin: options && options.origin + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj); } + if (obj.ranges != sel.ranges) { return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) } + else { return sel } + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + { sel = filterSelectionChange(doc, sel, options); } + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + { ensureCursorVisible(doc.cm); } + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) { return } + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) { out = sel.ranges.slice(0, i); } + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel + } + + function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { + var line = getLine(doc, pos.line); + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) { break } + else {--i; continue} + } + } + if (!m.atomic) { continue } + + if (oldPos) { + var near = m.find(dir < 0 ? 1 : -1), diff = (void 0); + if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) + { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); } + if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) + { return skipAtomicInner(doc, near, pos, dir, mayClear) } + } + + var far = m.find(dir < 0 ? -1 : 1); + if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) + { far = movePos(doc, far, dir, far.line == pos.line ? line : null); } + return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null + } + } } + return pos + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, oldPos, bias, mayClear) { + var dir = bias || 1; + var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || + skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); + if (!found) { + doc.cantEdit = true; + return Pos(doc.first, 0) + } + return found + } + + function movePos(doc, pos, dir, line) { + if (dir < 0 && pos.ch == 0) { + if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } + else { return null } + } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { + if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } + else { return null } + } else { + return new Pos(pos.line, pos.ch + dir) + } + } + + function selectAll(cm) { + cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll); + } + + // UPDATING + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function () { return obj.canceled = true; } + }; + if (update) { obj.update = function (from, to, text, origin) { + if (from) { obj.from = clipPos(doc, from); } + if (to) { obj.to = clipPos(doc, to); } + if (text) { obj.text = text; } + if (origin !== undefined) { obj.origin = origin; } + }; } + signal(doc, "beforeChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj); } + + if (obj.canceled) { return null } + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } + if (doc.cm.state.suppressEdits) { return } + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) { return } + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}); } + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + var suppress = doc.cm && doc.cm.state.suppressEdits; + if (suppress && !allowSelectionOnly) { return } + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + var i = 0; + for (; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + { break } + } + if (i == source.length) { return } + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return + } + selAfter = event; + } else if (suppress) { + source.push(event); + return + } else { break } + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + var loop = function ( i ) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return {} + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); } + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + }; + + for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { + var returned = loop( i$1 ); + + if ( returned ) return returned.v; + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) { return } + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( + Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch) + ); }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + { regLineChange(doc.cm, l, "gutter"); } + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return + } + if (change.from.line > doc.lastLine()) { return } + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) { selAfter = computeSelAfterChange(doc, change); } + if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } + else { updateDoc(doc, change, spans); } + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function (line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + { signalCursorActivity(cm); } + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function (line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) { cm.curOp.updateMaxLine = true; } + } + + retreatFrontier(doc, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + { regChange(cm); } + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + { regLineChange(cm, from.line, "text"); } + else + { regChange(cm, from.line, to.line + 1, lendiff); } + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) { signalLater(cm, "change", cm, obj); } + if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); } + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + var assign; + + if (!to) { to = from; } + if (cmp(to, from) < 0) { (assign = [to, from], from = assign[0], to = assign[1]); } + if (typeof code == "string") { code = doc.splitLines(code); } + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue + } + for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { + var cur = sub.changes[j$1]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)); } + else { no = lineNo(handle); } + if (no == null) { return null } + if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType); } + return line + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + var this$1 = this; + + this.lines = lines; + this.parent = null; + var height = 0; + for (var i = 0; i < lines.length; ++i) { + lines[i].parent = this$1; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length }, + + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + var this$1 = this; + + for (var i = at, e = at + n; i < e; ++i) { + var line = this$1.lines[i]; + this$1.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + var this$1 = this; + + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) { lines[i].parent = this$1; } + }, + + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + var this$1 = this; + + for (var e = at + n; at < e; ++at) + { if (op(this$1.lines[at])) { return true } } + } + }; + + function BranchChunk(children) { + var this$1 = this; + + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this$1; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size }, + + removeInner: function(at, n) { + var this$1 = this; + + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this$1.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this$1.height -= oldHeight - child.height; + if (sz == rm) { this$1.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) { break } + at = 0; + } else { at -= sz; } + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + + collapse: function(lines) { + var this$1 = this; + + for (var i = 0; i < this.children.length; ++i) { this$1.children[i].collapse(lines); } + }, + + insertInner: function(at, lines, height) { + var this$1 = this; + + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this$1.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. + // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. + var remaining = child.lines.length % 25 + 25; + for (var pos = remaining; pos < child.lines.length;) { + var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); + child.height -= leaf.height; + this$1.children.splice(++i, 0, leaf); + leaf.parent = this$1; + } + child.lines = child.lines.slice(0, remaining); + this$1.maybeSpill(); + } + break + } + at -= sz; + } + }, + + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) { return } + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10) + me.parent.maybeSpill(); + }, + + iterN: function(at, n, op) { + var this$1 = this; + + for (var i = 0; i < this.children.length; ++i) { + var child = this$1.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) { return true } + if ((n -= used) == 0) { break } + at = 0; + } else { at -= sz; } + } + } + }; + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = function(doc, node, options) { + var this$1 = this; + + if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) + { this$1[opt] = options[opt]; } } } + this.doc = doc; + this.node = node; + }; + + LineWidget.prototype.clear = function () { + var this$1 = this; + + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) { return } + for (var i = 0; i < ws.length; ++i) { if (ws[i] == this$1) { ws.splice(i--, 1); } } + if (!ws.length) { line.widgets = null; } + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) { + runInOp(cm, function () { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + signalLater(cm, "lineWidgetCleared", cm, this, no); + } + }; + + LineWidget.prototype.changed = function () { + var this$1 = this; + + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) { return } + if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff); } + if (cm) { + runInOp(cm, function () { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)); + }); + } + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + { addToScrollTop(cm, diff); } + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) { cm.display.alignWidgets = true; } + changeLine(doc, handle, "widget", function (line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) { widgets.push(widget); } + else { widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); } + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) { addToScrollTop(cm, widget.height); } + cm.curOp.forceUpdate = true; + } + return true + }); + if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)); } + return widget + } + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + var TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + + // Clear the marker. + TextMarker.prototype.clear = function () { + var this$1 = this; + + if (this.explicitlyCleared) { return } + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) { startOperation(cm); } + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) { signalLater(this, "clear", found.from, found.to); } + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this$1.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this$1); + if (cm && !this$1.collapsed) { regLineChange(cm, lineNo(line), "text"); } + else if (cm) { + if (span.to != null) { max = lineNo(line); } + if (span.from != null) { min = lineNo(line); } + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this$1.collapsed && !lineIsHidden(this$1.doc, line) && cm) + { updateLineHeight(line, textHeight(cm.display)); } + } + if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { + var visual = visualLine(this$1.lines[i$1]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } } + + if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1); } + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) { reCheckSelection(cm.doc); } + } + if (cm) { signalLater(cm, "markerCleared", cm, this, min, max); } + if (withOp) { endOperation(cm); } + if (this.parent) { this.parent.clear(); } + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function (side, lineObj) { + var this$1 = this; + + if (side == null && this.type == "bookmark") { side = 1; } + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this$1.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this$1); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) { return from } + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) { return to } + } + } + return from && {from: from, to: to} + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function () { + var this$1 = this; + + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) { return } + runInOp(cm, function () { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + { updateLineHeight(line, line.height + dHeight); } + } + signalLater(cm, "markerChanged", cm, this$1); + }); + }; + + TextMarker.prototype.attachLine = function (line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); } + } + this.lines.push(line); + }; + + TextMarker.prototype.detachLine = function (line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp + ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + eventMixin(TextMarker); + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) { return markTextShared(doc, from, to, options, type) } + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) { copyObj(options, marker, false); } + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + { return marker } + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true"); } + if (options.insertLeft) { marker.widgetNode.insertLeft = true; } + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + { throw new Error("Inserting collapsed marker partially overlapping an existing one") } + seeCollapsedSpans(); + } + + if (marker.addToHistory) + { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); } + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function (line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + { updateMaxLine = true; } + if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0); } + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { + if (lineIsHidden(doc, line)) { updateLineHeight(line, 0); } + }); } + + if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }); } + + if (marker.readOnly) { + seeReadOnlySpans(); + if (doc.history.done.length || doc.history.undone.length) + { doc.clearHistory(); } + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) { cm.curOp.updateMaxLine = true; } + if (marker.collapsed) + { regChange(cm, from.line, to.line + 1); } + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text"); } } + if (marker.atomic) { reCheckSelection(cm.doc); } + signalLater(cm, "markerAdded", cm, marker); + } + return marker + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = function(markers, primary) { + var this$1 = this; + + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + { markers[i].parent = this$1; } + }; + + SharedTextMarker.prototype.clear = function () { + var this$1 = this; + + if (this.explicitlyCleared) { return } + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + { this$1.markers[i].clear(); } + signalLater(this, "clear"); + }; + + SharedTextMarker.prototype.find = function (side, lineObj) { + return this.primary.find(side, lineObj) + }; + eventMixin(SharedTextMarker); + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function (doc) { + if (widget) { options.widgetNode = widget.cloneNode(true); } + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + { if (doc.linked[i].isParent) { return } } + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary) + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + var loop = function ( i ) { + var marker = markers[i], linked = [marker.primary.doc]; + linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + }; + + for (var i = 0; i < markers.length; i++) loop( i ); + } + + var nextDocId = 0; + var Doc = function(text, mode, firstLine, lineSep, direction) { + if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } + if (firstLine == null) { firstLine = 0; } + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.modeFrontier = this.highlightFrontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + this.lineSep = lineSep; + this.direction = (direction == "rtl") ? "rtl" : "ltr"; + this.extend = false; + + if (typeof text == "string") { text = this.splitLines(text); } + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) { this.iterN(from - this.first, to - from, op); } + else { this.iterN(this.first, this.first + this.size, from); } + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) { height += lines[i].height; } + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: this.splitLines(code), origin: "setValue", full: true}, true); + if (this.cm) { scrollToCoords(this.cm, 0, 0); } + setSelection(this, simpleSelection(top), sel_dontScroll); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, + + getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, + getLineNumber: function(line) {return lineNo(line)}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") { line = getLine(this, line); } + return visualLine(line) + }, + + lineCount: function() {return this.size}, + firstLine: function() {return this.first}, + lastLine: function() {return this.first + this.size - 1}, + + clipPos: function(pos) {return clipPos(this, pos)}, + + getCursor: function(start) { + var range$$1 = this.sel.primary(), pos; + if (start == null || start == "head") { pos = range$$1.head; } + else if (start == "anchor") { pos = range$$1.anchor; } + else if (start == "end" || start == "to" || start === false) { pos = range$$1.to(); } + else { pos = range$$1.from(); } + return pos + }, + listSelections: function() { return this.sel.ranges }, + somethingSelected: function() {return this.sel.somethingSelected()}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads), options); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + var heads = map(this.sel.ranges, f); + extendSelections(this, clipPosArray(this, heads), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + var this$1 = this; + + if (!ranges.length) { return } + var out = []; + for (var i = 0; i < ranges.length; i++) + { out[i] = new Range(clipPos(this$1, ranges[i].anchor), + clipPos(this$1, ranges[i].head)); } + if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex); } + setSelection(this, normalizeSelection(this.cm, out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var this$1 = this; + + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this$1, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) { return lines } + else { return lines.join(lineSep || this.lineSeparator()) } + }, + getSelections: function(lineSep) { + var this$1 = this; + + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this$1, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) { sel = sel.join(lineSep || this$1.lineSeparator()); } + parts[i] = sel; + } + return parts + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + { dup[i] = code; } + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var this$1 = this; + + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range$$1 = sel.ranges[i]; + changes[i] = {from: range$$1.from(), to: range$$1.to(), text: this$1.splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) + { makeChange(this$1, changes[i$1]); } + if (newSel) { setSelectionReplaceHistory(this, newSel); } + else if (this.cm) { ensureCursorVisible(this.cm); } + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done; } } + for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone; } } + return {undo: done, redo: undone} + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; } + return this.history.generation + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration) + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)} + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + setGutterMarker: docMethodOp(function(line, gutterID, value) { + return changeLine(this, line, "gutter", function (line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) { line.gutterMarkers = null; } + return true + }) + }), + + clearGutter: docMethodOp(function(gutterID) { + var this$1 = this; + + this.iter(function (line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + changeLine(this$1, line, "gutter", function () { + line.gutterMarkers[gutterID] = null; + if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null; } + return true + }); + } + }); + }), + + lineInfo: function(line) { + var n; + if (typeof line == "number") { + if (!isLine(this, line)) { return null } + n = line; + line = getLine(this, line); + if (!line) { return null } + } else { + n = lineNo(line); + if (n == null) { return null } + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets} + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) { line[prop] = cls; } + else if (classTest(cls).test(line[prop])) { return false } + else { line[prop] += " " + cls; } + return true + }) + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) { return false } + else if (cls == null) { line[prop] = null; } + else { + var found = cur.match(classTest(cls)); + if (!found) { return false } + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true + }) + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options) + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark") + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + { markers.push(span.marker.parent || span.marker); } + } } + return markers + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo$$1 = from.line; + this.iter(from.line, to.line + 1, function (line) { + var spans = line.markedSpans; + if (spans) { for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(span.to != null && lineNo$$1 == from.line && from.ch >= span.to || + span.from == null && lineNo$$1 != from.line || + span.from != null && lineNo$$1 == to.line && span.from >= to.ch) && + (!filter || filter(span.marker))) + { found.push(span.marker.parent || span.marker); } + } } + ++lineNo$$1; + }); + return found + }, + getAllMarks: function() { + var markers = []; + this.iter(function (line) { + var sps = line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) + { if (sps[i].from != null) { markers.push(sps[i].marker); } } } + }); + return markers + }, + + posFromIndex: function(off) { + var ch, lineNo$$1 = this.first, sepSize = this.lineSeparator().length; + this.iter(function (line) { + var sz = line.text.length + sepSize; + if (sz > off) { ch = off; return true } + off -= sz; + ++lineNo$$1; + }); + return clipPos(this, Pos(lineNo$$1, ch)) + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) { return 0 } + var sepSize = this.lineSeparator().length; + this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value + index += line.text.length + sepSize; + }); + return index + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), + this.modeOption, this.first, this.lineSep, this.direction); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc + }, + + linkedDoc: function(options) { + if (!options) { options = {}; } + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) { from = options.from; } + if (options.to != null && options.to < to) { to = options.to; } + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction); + if (options.sharedHist) { copy.history = this.history + ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy + }, + unlinkDoc: function(other) { + var this$1 = this; + + if (other instanceof CodeMirror) { other = other.doc; } + if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { + var link = this$1.linked[i]; + if (link.doc != other) { continue } + this$1.linked.splice(i, 1); + other.unlinkDoc(this$1); + detachSharedMarkers(findSharedMarkers(this$1)); + break + } } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode}, + getEditor: function() {return this.cm}, + + splitLines: function(str) { + if (this.lineSep) { return str.split(this.lineSep) } + return splitLinesAuto(str) + }, + lineSeparator: function() { return this.lineSep || "\n" }, + + setDirection: docMethodOp(function (dir) { + if (dir != "rtl") { dir = "ltr"; } + if (dir == this.direction) { return } + this.direction = dir; + this.iter(function (line) { return line.order = null; }); + if (this.cm) { directionChanged(this.cm); } + }) + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + clearDragCursor(cm); + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + { return } + e_preventDefault(e); + if (ie) { lastDrop = +new Date; } + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || cm.isReadOnly()) { return } + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function (file, i) { + if (cm.options.allowDropFileTypes && + indexOf(cm.options.allowDropFileTypes, file.type) == -1) + { return } + + var reader = new FileReader; + reader.onload = operation(cm, function () { + var content = reader.result; + if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { content = ""; } + text[i] = content; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, + text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())), + origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) { loadFile(files[i], i); } + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function () { return cm.display.input.focus(); }, 20); + return + } + try { + var text$1 = e.dataTransfer.getData("Text"); + if (text$1) { + var selected; + if (cm.state.draggingText && !cm.state.draggingText.copy) + { selected = cm.listSelections(); } + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) + { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag"); } } + cm.replaceSelection(text$1, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } + + e.dataTransfer.setData("Text", cm.getSelection()); + e.dataTransfer.effectAllowed = "copyMove"; + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) { img.parentNode.removeChild(img); } + } + } + + function onDragOver(cm, e) { + var pos = posFromMouse(cm, e); + if (!pos) { return } + var frag = document.createDocumentFragment(); + drawSelectionCursor(cm, pos, frag); + if (!cm.display.dragCursor) { + cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); + cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); + } + removeChildrenAndAdd(cm.display.dragCursor, frag); + } + + function clearDragCursor(cm) { + if (cm.display.dragCursor) { + cm.display.lineSpace.removeChild(cm.display.dragCursor); + cm.display.dragCursor = null; + } + } + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.getElementsByClassName) { return } + var byClass = document.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) { f(cm); } + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) { return } + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function () { + if (resizeTimer == null) { resizeTimer = setTimeout(function () { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); } + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function () { return forEachCodeMirror(onBlur); }); + } + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + var keyNames = { + 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", 145: "ScrollLock", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" + }; + + // Number keys + for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i); } + // Alphabetic keys + for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1); } + // Function keys + for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2; } + + var keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + "fallthrough": "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", + "Ctrl-O": "openLine" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + "fallthrough": ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/); + name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } + else if (/^a(lt)?$/i.test(mod)) { alt = true; } + else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } + else if (/^s(hift)?$/i.test(mod)) { shift = true; } + else { throw new Error("Unrecognized modifier name: " + mod) } + } + if (alt) { name = "Alt-" + name; } + if (ctrl) { name = "Ctrl-" + name; } + if (cmd) { name = "Cmd-" + name; } + if (shift) { name = "Shift-" + name; } + return name + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + function normalizeKeyMap(keymap) { + var copy = {}; + for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } + if (value == "...") { delete keymap[keyname]; continue } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val = (void 0), name = (void 0); + if (i == keys.length - 1) { + name = keys.join(" "); + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) { copy[name] = val; } + else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } + } + delete keymap[keyname]; + } } + for (var prop in copy) { keymap[prop] = copy[prop]; } + return keymap + } + + function lookupKey(key, map$$1, handle, context) { + map$$1 = getKeyMap(map$$1); + var found = map$$1.call ? map$$1.call(key, context) : map$$1[key]; + if (found === false) { return "nothing" } + if (found === "...") { return "multi" } + if (found != null && handle(found)) { return "handled" } + + if (map$$1.fallthrough) { + if (Object.prototype.toString.call(map$$1.fallthrough) != "[object Array]") + { return lookupKey(key, map$$1.fallthrough, handle, context) } + for (var i = 0; i < map$$1.fallthrough.length; i++) { + var result = lookupKey(key, map$$1.fallthrough[i], handle, context); + if (result) { return result } + } + } + } + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + function isModifierKey(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" + } + + function addModifierNames(name, event, noShift) { + var base = name; + if (event.altKey && base != "Alt") { name = "Alt-" + name; } + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") { name = "Cmd-" + name; } + if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } + return name + } + + // Look up the name of a key as indicated by an event object. + function keyName(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) { return false } + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) { return false } + // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, + // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) + if (event.keyCode == 3 && event.code) { name = event.code; } + return addModifierNames(name, event, noShift) + } + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function () { + for (var i = kill.length - 1; i >= 0; i--) + { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); } + ensureCursorVisible(cm); + }); + } + + function moveCharLogically(line, ch, dir) { + var target = skipExtendingChars(line.text, ch + dir, dir); + return target < 0 || target > line.text.length ? null : target + } + + function moveLogically(line, start, dir) { + var ch = moveCharLogically(line, start.ch, dir); + return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") + } + + function endOfLine(visually, cm, lineObj, lineNo, dir) { + if (visually) { + var order = getOrder(lineObj, cm.doc.direction); + if (order) { + var part = dir < 0 ? lst(order) : order[0]; + var moveInStorageOrder = (dir < 0) == (part.level == 1); + var sticky = moveInStorageOrder ? "after" : "before"; + var ch; + // With a wrapped rtl chunk (possibly spanning multiple bidi parts), + // it could be that the last bidi part is not on the last visual line, + // since visual lines contain content order-consecutive chunks. + // Thus, in rtl, we are looking for the first (content-order) character + // in the rtl chunk that is on the last line (that is, the same line + // as the last (content-order) character). + if (part.level > 0 || cm.doc.direction == "rtl") { + var prep = prepareMeasureForLine(cm, lineObj); + ch = dir < 0 ? lineObj.text.length - 1 : 0; + var targetTop = measureCharPrepared(cm, prep, ch).top; + ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch); + if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1); } + } else { ch = dir < 0 ? part.to : part.from; } + return new Pos(lineNo, ch, sticky) + } + } + return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") + } + + function moveVisually(cm, line, start, dir) { + var bidi = getOrder(line, cm.doc.direction); + if (!bidi) { return moveLogically(line, start, dir) } + if (start.ch >= line.text.length) { + start.ch = line.text.length; + start.sticky = "before"; + } else if (start.ch <= 0) { + start.ch = 0; + start.sticky = "after"; + } + var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]; + if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { + // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, + // nothing interesting happens. + return moveLogically(line, start, dir) + } + + var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }; + var prep; + var getWrappedLineExtent = function (ch) { + if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } + prep = prep || prepareMeasureForLine(cm, line); + return wrappedLineExtentChar(cm, line, prep, ch) + }; + var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch); + + if (cm.doc.direction == "rtl" || part.level == 1) { + var moveInStorageOrder = (part.level == 1) == (dir < 0); + var ch = mv(start, moveInStorageOrder ? 1 : -1); + if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { + // Case 2: We move within an rtl part or in an rtl editor on the same visual line + var sticky = moveInStorageOrder ? "before" : "after"; + return new Pos(start.line, ch, sticky) + } + } + + // Case 3: Could not move within this bidi part in this visual line, so leave + // the current bidi part + + var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { + var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder + ? new Pos(start.line, mv(ch, 1), "before") + : new Pos(start.line, ch, "after"); }; + + for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { + var part = bidi[partPos]; + var moveInStorageOrder = (dir > 0) == (part.level != 1); + var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1); + if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } + ch = moveInStorageOrder ? part.from : mv(part.to, -1); + if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } + } + }; + + // Case 3a: Look for other bidi parts on the same visual line + var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent); + if (res) { return res } + + // Case 3b: Look for other bidi parts on the next visual line + var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1); + if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { + res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)); + if (res) { return res } + } + + // Case 4: Nowhere to move + return null + } + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = { + selectAll: selectAll, + singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, + killLine: function (cm) { return deleteNearSelection(cm, function (range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + { return {from: range.head, to: Pos(range.head.line + 1, 0)} } + else + { return {from: range.head, to: Pos(range.head.line, len)} } + } else { + return {from: range.from(), to: range.to()} + } + }); }, + deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) + }); }); }, + delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), to: range.from() + }); }); }, + delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()} + }); }, + delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos } + }); }, + undo: function (cm) { return cm.undo(); }, + redo: function (cm) { return cm.redo(); }, + undoSelection: function (cm) { return cm.undoSelection(); }, + redoSelection: function (cm) { return cm.redoSelection(); }, + goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, + goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, + goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1} + ); }, + goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, + {origin: "+move", bias: 1} + ); }, + goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1} + ); }, + goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") + }, sel_move); }, + goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div") + }, sel_move); }, + goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } + return pos + }, sel_move); }, + goLineUp: function (cm) { return cm.moveV(-1, "line"); }, + goLineDown: function (cm) { return cm.moveV(1, "line"); }, + goPageUp: function (cm) { return cm.moveV(-1, "page"); }, + goPageDown: function (cm) { return cm.moveV(1, "page"); }, + goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, + goCharRight: function (cm) { return cm.moveH(1, "char"); }, + goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, + goColumnRight: function (cm) { return cm.moveH(1, "column"); }, + goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, + goGroupRight: function (cm) { return cm.moveH(1, "group"); }, + goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, + goWordRight: function (cm) { return cm.moveH(1, "word"); }, + delCharBefore: function (cm) { return cm.deleteH(-1, "char"); }, + delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, + delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, + delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, + delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, + delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, + indentAuto: function (cm) { return cm.indentSelection("smart"); }, + indentMore: function (cm) { return cm.indentSelection("add"); }, + indentLess: function (cm) { return cm.indentSelection("subtract"); }, + insertTab: function (cm) { return cm.replaceSelection("\t"); }, + insertSoftTab: function (cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(spaceStr(tabSize - col % tabSize)); + } + cm.replaceSelections(spaces); + }, + defaultTab: function (cm) { + if (cm.somethingSelected()) { cm.indentSelection("add"); } + else { cm.execCommand("insertTab"); } + }, + // Swap the two chars left and right of each selection's head. + // Move cursor behind the two swapped characters afterwards. + // + // Doesn't consider line feeds a character. + // Doesn't scan more than one line above to find a character. + // Doesn't do anything on an empty line. + // Doesn't do anything with non-empty selections. + transposeChars: function (cm) { return runInOp(cm, function () { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) { continue } + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1); } + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) { + cur = new Pos(cur.line, 1); + cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), cur, "+transpose"); + } + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); }, + newlineAndIndent: function (cm) { return runInOp(cm, function () { + var sels = cm.listSelections(); + for (var i = sels.length - 1; i >= 0; i--) + { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input"); } + sels = cm.listSelections(); + for (var i$1 = 0; i$1 < sels.length; i$1++) + { cm.indentLine(sels[i$1].from().line, null, true); } + ensureCursorVisible(cm); + }); }, + openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, + toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } + }; + + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, visual, lineN, 1) + } + function lineEnd(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLineEnd(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, line, lineN, -1) + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line, cm.doc.direction); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) + } + return start + } + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) { return false } + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + if (dropShift) { cm.display.shift = false; } + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) { return result } + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm) + } + + // Note that, despite the name, this function is also used to check + // for bound mouse clicks. + + var stopSeq = new Delayed; + + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) { return "handled" } + if (/\'$/.test(name)) + { cm.state.keySeq = null; } + else + { stopSeq.set(50, function () { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); } + if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } + } + return dispatchKeyInner(cm, name, e, handle) + } + + function dispatchKeyInner(cm, name, e, handle) { + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + { cm.state.keySeq = name; } + if (result == "handled") + { signalLater(cm, "keyHandled", cm, name, e); } + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + return !!result + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) { return false } + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) + || dispatchKey(cm, name, e, function (b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + { return doHandleBinding(cm, b) } + }) + } else { + return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) { return } + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false; } + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + { cm.replaceSelection("", null, "cut"); } + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + { showCrossHair(cm); } + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) { this.doc.sel.shift = false; } + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + // Some browsers fire keypress events for backspace + if (ch == "\x08") { return } + if (handleCharBinding(cm, e, ch)) { return } + cm.display.input.onKeyPress(e); + } + + var DOUBLECLICK_DELAY = 400; + + var PastClick = function(time, pos, button) { + this.time = time; + this.pos = pos; + this.button = button; + }; + + PastClick.prototype.compare = function (time, pos, button) { + return this.time + DOUBLECLICK_DELAY > time && + cmp(pos, this.pos) == 0 && button == this.button + }; + + var lastClick, lastDoubleClick; + function clickRepeat(pos, button) { + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { + lastClick = lastDoubleClick = null; + return "triple" + } else if (lastClick && lastClick.compare(now, pos, button)) { + lastDoubleClick = new PastClick(now, pos, button); + lastClick = null; + return "double" + } else { + lastClick = new PastClick(now, pos, button); + lastDoubleClick = null; + return "single" + } + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } + display.input.ensurePolled(); + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function () { return display.scroller.draggable = true; }, 100); + } + return + } + if (clickInGutter(cm, e)) { return } + var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"; + window.focus(); + + // #3261: make sure, that we're not starting a second selection + if (button == 1 && cm.state.selectingText) + { cm.state.selectingText(e); } + + if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } + + if (button == 1) { + if (pos) { leftButtonDown(cm, pos, repeat, e); } + else if (e_target(e) == display.scroller) { e_preventDefault(e); } + } else if (button == 2) { + if (pos) { extendSelection(cm.doc, pos); } + setTimeout(function () { return display.input.focus(); }, 20); + } else if (button == 3) { + if (captureRightClick) { cm.display.input.onContextMenu(e); } + else { delayBlurEvent(cm); } + } + } + + function handleMappedButton(cm, button, pos, repeat, event) { + var name = "Click"; + if (repeat == "double") { name = "Double" + name; } + else if (repeat == "triple") { name = "Triple" + name; } + name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name; + + return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { + if (typeof bound == "string") { bound = commands[bound]; } + if (!bound) { return false } + var done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + done = bound(cm, pos) != Pass; + } finally { + cm.state.suppressEdits = false; + } + return done + }) + } + + function configureMouse(cm, repeat, event) { + var option = cm.getOption("configureMouse"); + var value = option ? option(cm, repeat, event) : {}; + if (value.unit == null) { + var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey; + value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"; + } + if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey; } + if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey; } + if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey); } + return value + } + + function leftButtonDown(cm, pos, repeat, event) { + if (ie) { setTimeout(bind(ensureFocus, cm), 0); } + else { cm.curOp.focus = activeElt(); } + + var behavior = configureMouse(cm, repeat, event); + + var sel = cm.doc.sel, contained; + if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && + repeat == "single" && (contained = sel.contains(pos)) > -1 && + (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && + (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) + { leftButtonStartDrag(cm, event, pos, behavior); } + else + { leftButtonSelect(cm, event, pos, behavior); } + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, event, pos, behavior) { + var display = cm.display, moved = false; + var dragEnd = operation(cm, function (e) { + if (webkit) { display.scroller.draggable = false; } + cm.state.draggingText = false; + off(display.wrapper.ownerDocument, "mouseup", dragEnd); + off(display.wrapper.ownerDocument, "mousemove", mouseMove); + off(display.scroller, "dragstart", dragStart); + off(display.scroller, "drop", dragEnd); + if (!moved) { + e_preventDefault(e); + if (!behavior.addNew) + { extendSelection(cm.doc, pos, null, null, behavior.extend); } + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if (webkit || ie && ie_version == 9) + { setTimeout(function () {display.wrapper.ownerDocument.body.focus(); display.input.focus();}, 20); } + else + { display.input.focus(); } + } + }); + var mouseMove = function(e2) { + moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10; + }; + var dragStart = function () { return moved = true; }; + // Let the drag handler handle this. + if (webkit) { display.scroller.draggable = true; } + cm.state.draggingText = dragEnd; + dragEnd.copy = !behavior.moveOnDrag; + // IE's approach to draggable + if (display.scroller.dragDrop) { display.scroller.dragDrop(); } + on(display.wrapper.ownerDocument, "mouseup", dragEnd); + on(display.wrapper.ownerDocument, "mousemove", mouseMove); + on(display.scroller, "dragstart", dragStart); + on(display.scroller, "drop", dragEnd); + + delayBlurEvent(cm); + setTimeout(function () { return display.input.focus(); }, 20); + } + + function rangeForUnit(cm, pos, unit) { + if (unit == "char") { return new Range(pos, pos) } + if (unit == "word") { return cm.findWordAt(pos) } + if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } + var result = unit(cm, pos); + return new Range(result.from, result.to) + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, event, start, behavior) { + var display = cm.display, doc = cm.doc; + e_preventDefault(event); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (behavior.addNew && !behavior.extend) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + { ourRange = ranges[ourIndex]; } + else + { ourRange = new Range(start, start); } + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (behavior.unit == "rectangle") { + if (!behavior.addNew) { ourRange = new Range(start, start); } + start = posFromMouse(cm, event, true, true); + ourIndex = -1; + } else { + var range$$1 = rangeForUnit(cm, start, behavior.unit); + if (behavior.extend) + { ourRange = extendRange(ourRange, range$$1.anchor, range$$1.head, behavior.extend); } + else + { ourRange = range$$1; } + } + + if (!behavior.addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { + setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), + {scroll: false, origin: "*mouse"}); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) { return } + lastPos = pos; + + if (behavior.unit == "rectangle") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); } + else if (text.length > leftPos) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } + } + if (!ranges.length) { ranges.push(new Range(start, start)); } + setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var range$$1 = rangeForUnit(cm, pos, behavior.unit); + var anchor = oldRange.anchor, head; + if (cmp(range$$1.anchor, anchor) > 0) { + head = range$$1.head; + anchor = minPos(oldRange.from(), range$$1.anchor); + } else { + head = range$$1.anchor; + anchor = maxPos(oldRange.to(), range$$1.head); + } + var ranges$1 = startSel.ranges.slice(0); + ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)); + setSelection(doc, normalizeSelection(cm, ranges$1, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle"); + if (!cur) { return } + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e); }}), 150); } + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) { setTimeout(operation(cm, function () { + if (counter != curCount) { return } + display.scroller.scrollTop += outside; + extend(e); + }), 50); } + } + } + + function done(e) { + cm.state.selectingText = false; + counter = Infinity; + e_preventDefault(e); + display.input.focus(); + off(display.wrapper.ownerDocument, "mousemove", move); + off(display.wrapper.ownerDocument, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function (e) { + if (e.buttons === 0 || !e_button(e)) { done(e); } + else { extend(e); } + }); + var up = operation(cm, done); + cm.state.selectingText = up; + on(display.wrapper.ownerDocument, "mousemove", move); + on(display.wrapper.ownerDocument, "mouseup", up); + } + + // Used when mouse-selecting to adjust the anchor to the proper side + // of a bidi jump depending on the visual position of the head. + function bidiSimplify(cm, range$$1) { + var anchor = range$$1.anchor; + var head = range$$1.head; + var anchorLine = getLine(cm.doc, anchor.line); + if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range$$1 } + var order = getOrder(anchorLine); + if (!order) { return range$$1 } + var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]; + if (part.from != anchor.ch && part.to != anchor.ch) { return range$$1 } + var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1); + if (boundary == 0 || boundary == order.length) { return range$$1 } + + // Compute the relative visual position of the head compared to the + // anchor (<0 is to the left, >0 to the right) + var leftSide; + if (head.line != anchor.line) { + leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0; + } else { + var headIndex = getBidiPartAt(order, head.ch, head.sticky); + var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1); + if (headIndex == boundary - 1 || headIndex == boundary) + { leftSide = dir < 0; } + else + { leftSide = dir > 0; } + } + + var usePart = order[boundary + (leftSide ? -1 : 0)]; + var from = leftSide == (usePart.level == 1); + var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"; + return anchor.ch == ch && anchor.sticky == sticky ? range$$1 : new Range(new Pos(anchor.line, ch, sticky), head) + } + + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent) { + var mX, mY; + if (e.touches) { + mX = e.touches[0].clientX; + mY = e.touches[0].clientY; + } else { + try { mX = e.clientX; mY = e.clientY; } + catch(e) { return false } + } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } + if (prevent) { e_preventDefault(e); } + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signal(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e) + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true) + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } + if (signalDOMEvent(cm, e, "contextmenu")) { return } + if (!captureRightClick) { cm.display.input.onContextMenu(e); } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) { return false } + return gutterEvent(cm, e, "gutterContextMenu", false) + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + var Init = {toString: function(){return "CodeMirror.Init"}}; + + var defaults = {}; + var optionHandlers = {}; + + function defineOptions(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) { optionHandlers[name] = + notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old); }} : handle; } + } + + CodeMirror.defineOption = option; + + // Passed to option handlers when there is no old value. + CodeMirror.Init = Init; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function (cm, val) { return cm.setValue(val); }, true); + option("mode", null, function (cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function (cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + + option("lineSeparator", null, function (cm, val) { + cm.doc.lineSep = val; + if (!val) { return } + var newBreaks = [], lineNo = cm.doc.first; + cm.doc.iter(function (line) { + for (var pos = 0;;) { + var found = line.text.indexOf(val, pos); + if (found == -1) { break } + pos = found + val.length; + newBreaks.push(Pos(lineNo, found)); + } + lineNo++; + }); + for (var i = newBreaks.length - 1; i >= 0; i--) + { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); } + }); + option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g, function (cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != Init) { cm.refresh(); } + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function () { + throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME + }, true); + option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function (cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function (cm, val, old) { + var next = getKeyMap(val); + var prev = old != Init && getKeyMap(old); + if (prev && prev.detach) { prev.detach(cm, next); } + if (next.attach) { next.attach(cm, prev || null); } + }); + option("extraKeys", null); + option("configureMouse", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function (cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function (cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true); + option("scrollbarStyle", "native", function (cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function (cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function (integer) { return integer; }, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + option("pasteLinesPerSelection", true); + option("selectionsMayTouch", false); + + option("readOnly", false, function (cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + } + cm.display.input.readOnlyChanged(val); + }); + option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset(); }}, true); + option("dragDrop", true, dragDropChanged); + option("allowDropFileTypes", null); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function (cm, val) { + if (!val) { cm.display.input.resetPosition(); } + }); + + option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }); + option("autofocus", null); + option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true); + option("phrases", null); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + alignHorizontally(cm); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.enter); + toggle(cm.display.scroller, "dragover", funcs.over); + toggle(cm.display.scroller, "dragleave", funcs.leave); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function () { return updateScrollbars(cm); }, 100); + } + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + var this$1 = this; + + if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction); } + else if (options.mode) { doc.modeOption = options.mode; } + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + { this.display.wrapper.className += " CodeMirror-wrap"; } + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + selectingText: false, + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + if (options.autofocus && !mobile) { display.input.focus(); } + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20); } + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || this.hasFocus()) + { setTimeout(bind(onFocus, this), 20); } + else + { onBlur(this); } + + for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) + { optionHandlers[opt](this$1, options[opt], Init); } } + maybeUpdateLineNumberWidth(this); + if (options.finishInit) { options.finishInit(this); } + for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this$1); } + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + { display.lineDiv.style.textRendering = "auto"; } + } + + // The default configuration options. + CodeMirror.defaults = defaults; + // Functions to run when options are changed. + CodeMirror.optionHandlers = optionHandlers; + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + { on(d.scroller, "dblclick", operation(cm, function (e) { + if (signalDOMEvent(cm, e)) { return } + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); } + else + { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }); } + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + } + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) { return false } + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1 + } + function farAway(touch, other) { + if (other.left == null) { return true } + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20 + } + on(d.scroller, "touchstart", function (e) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { + d.input.ensurePolled(); + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function () { + if (d.activeTouch) { d.activeTouch.moved = true; } + }); + on(d.scroller, "touchend", function (e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + { range = new Range(pos, pos); } + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + { range = cm.findWordAt(pos); } + else // Triple tap + { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); } + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function () { + if (d.scroller.clientHeight) { + updateScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }); + on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e); }}, + over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, + start: function (e) { return onDragStart(cm, e); }, + drop: operation(cm, onDrop), + leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} + }; + + var inp = d.input.getField(); + on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", function (e) { return onFocus(cm, e); }); + on(inp, "blur", function (e) { return onBlur(cm, e); }); + } + + var initHooks = []; + CodeMirror.defineInitHook = function (f) { return initHooks.push(f); }; + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) { how = "add"; } + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) { how = "prev"; } + else { state = getContextBefore(cm, n).state; } + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) { line.stateAfter = null; } + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) { return } + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize); } + else { indentation = 0; } + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} } + if (pos < indentation) { indentString += spaceStr(indentation - pos); } + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + return true + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { + var range = doc.sel.ranges[i$1]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos$1 = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)); + break + } + } + } + } + + // This will be set to a {lineWise: bool, text: [string]} object, so + // that, when pasting, we know what kind of selections the copied + // text was made out of. + var lastCopied = null; + + function setLastCopied(newLastCopied) { + lastCopied = newLastCopied; + } + + function applyTextInput(cm, inserted, deleted, sel, origin) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) { sel = doc.sel; } + + var paste = cm.state.pasteIncoming || origin == "paste"; + var textLines = splitLinesAuto(inserted), multiPaste = null; + // When pasting N lines into N selections, insert one line per selection + if (paste && sel.ranges.length > 1) { + if (lastCopied && lastCopied.text.join("\n") == inserted) { + if (sel.ranges.length % lastCopied.text.length == 0) { + multiPaste = []; + for (var i = 0; i < lastCopied.text.length; i++) + { multiPaste.push(doc.splitLines(lastCopied.text[i])); } + } + } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { + multiPaste = map(textLines, function (l) { return [l]; }); + } + } + + var updateInput; + // Normal behavior is to insert the new text into every selection + for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { + var range$$1 = sel.ranges[i$1]; + var from = range$$1.from(), to = range$$1.to(); + if (range$$1.empty()) { + if (deleted && deleted > 0) // Handle deletion + { from = Pos(from.line, from.ch - deleted); } + else if (cm.state.overwrite && !paste) // Handle overwrite + { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); } + else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted) + { from = to = Pos(from.line, 0); } + } + updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, + origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + } + if (inserted && !paste) + { triggerElectric(cm, inserted); } + + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } + + function handlePaste(e, cm) { + var pasted = e.clipboardData && e.clipboardData.getData("Text"); + if (pasted) { + e.preventDefault(); + if (!cm.isReadOnly() && !cm.options.disableInput) + { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }); } + return true + } + } + + function triggerElectric(cm, inserted) { + // When an 'electric' character is inserted, immediately trigger a reindent + if (!cm.options.electricChars || !cm.options.smartIndent) { return } + var sel = cm.doc.sel; + + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range$$1 = sel.ranges[i]; + if (range$$1.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range$$1.head.line)) { continue } + var mode = cm.getModeAt(range$$1.head); + var indented = false; + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indented = indentLine(cm, range$$1.head.line, "smart"); + break + } } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(cm.doc, range$$1.head.line).text.slice(0, range$$1.head.ch))) + { indented = indentLine(cm, range$$1.head.line, "smart"); } + } + if (indented) { signalLater(cm, "electricInput", cm, range$$1.head.line); } + } + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges} + } + + function disableBrowserMagic(field, spellcheck) { + field.setAttribute("autocorrect", "off"); + field.setAttribute("autocapitalize", "off"); + field.setAttribute("spellcheck", !!spellcheck); + } + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) { te.style.width = "1000px"; } + else { te.setAttribute("wrap", "off"); } + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) { te.style.border = "1px solid black"; } + disableBrowserMagic(te); + return div + } + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + function addEditorMethods(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + var helpers = CodeMirror.helpers = {}; + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") { return } + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + { operation(this, optionHandlers[option])(this, value, old); } + signal(this, "optionChange", this, option); + }, + + getOption: function(option) {return this.options[option]}, + getDoc: function() {return this.doc}, + + addKeyMap: function(map$$1, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map$$1)); + }, + removeKeyMap: function(map$$1) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + { if (maps[i] == map$$1 || maps[i].name == map$$1) { + maps.splice(i, 1); + return true + } } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) { throw new Error("Overlays may not be stateful.") } + insertSorted(this.state.overlays, + {mode: mode, modeSpec: spec, opaque: options && options.opaque, + priority: (options && options.priority) || 0}, + function (overlay) { return overlay.priority; }); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var this$1 = this; + + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this$1.state.modeGen++; + regChange(this$1); + return + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev"; } + else { dir = dir ? "add" : "subtract"; } + } + if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive); } + }), + indentSelection: methodOp(function(how) { + var this$1 = this; + + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range$$1 = ranges[i]; + if (!range$$1.empty()) { + var from = range$$1.from(), to = range$$1.to(); + var start = Math.max(end, from.line); + end = Math.min(this$1.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + { indentLine(this$1, j, how); } + var newRanges = this$1.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + { replaceOneSelection(this$1.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); } + } else if (range$$1.head.line > end) { + indentLine(this$1, range$$1.head.line, how, true); + end = range$$1.head.line; + if (i == this$1.doc.sel.primIndex) { ensureCursorVisible(this$1); } + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise) + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true) + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) { type = styles[2]; } + else { for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid; } + else if (styles[mid * 2 + 1] < ch) { before = mid + 1; } + else { type = styles[mid * 2 + 2]; break } + } } + var cut = type ? type.indexOf("overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) { return mode } + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0] + }, + + getHelpers: function(pos, type) { + var this$1 = this; + + var found = []; + if (!helpers.hasOwnProperty(type)) { return found } + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) { found.push(help[mode[type]]); } + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) { found.push(val); } + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i$1 = 0; i$1 < help._global.length; i$1++) { + var cur = help._global[i$1]; + if (cur.pred(mode, this$1) && indexOf(found, cur.val) == -1) + { found.push(cur.val); } + } + return found + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getContextBefore(this, line + 1, precise).state + }, + + cursorCoords: function(start, mode) { + var pos, range$$1 = this.doc.sel.primary(); + if (start == null) { pos = range$$1.head; } + else if (typeof start == "object") { pos = clipPos(this.doc, start); } + else { pos = start ? range$$1.from() : range$$1.to(); } + return cursorCoords(this, pos, mode || "page") + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page") + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top) + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset) + }, + heightAtLine: function(line, mode, includeWidgets) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) { line = this.doc.first; } + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + + (end ? this.doc.height - heightAtLine(lineObj) : 0) + }, + + defaultTextHeight: function() { return textHeight(this.display) }, + defaultCharWidth: function() { return charWidth(this.display) }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + { top = pos.top - node.offsetHeight; } + else if (pos.bottom + node.offsetHeight <= vspace) + { top = pos.bottom; } + if (left + node.offsetWidth > hspace) + { left = hspace - node.offsetWidth; } + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") { left = 0; } + else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2; } + node.style.left = left + "px"; + } + if (scroll) + { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}); } + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + triggerOnMouseDown: methodOp(onMouseDown), + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + { return commands[cmd].call(null, this) } + }, + + triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), + + findPosH: function(from, amount, unit, visually) { + var this$1 = this; + + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + cur = findPosH(this$1.doc, cur, dir, unit, visually); + if (cur.hitSide) { break } + } + return cur + }, + + moveH: methodOp(function(dir, unit) { + var this$1 = this; + + this.extendSelectionsBy(function (range$$1) { + if (this$1.display.shift || this$1.doc.extend || range$$1.empty()) + { return findPosH(this$1.doc, range$$1.head, dir, unit, this$1.options.rtlMoveVisually) } + else + { return dir < 0 ? range$$1.from() : range$$1.to() } + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + { doc.replaceSelection("", null, "+delete"); } + else + { deleteNearSelection(this, function (range$$1) { + var other = findPosH(doc, range$$1.head, dir, unit, false); + return dir < 0 ? {from: other, to: range$$1.head} : {from: range$$1.head, to: other} + }); } + }), + + findPosV: function(from, amount, unit, goalColumn) { + var this$1 = this; + + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + var coords = cursorCoords(this$1, cur, "div"); + if (x == null) { x = coords.left; } + else { coords.left = x; } + cur = findPosV(this$1, coords, dir, unit); + if (cur.hitSide) { break } + } + return cur + }, + + moveV: methodOp(function(dir, unit) { + var this$1 = this; + + var doc = this.doc, goals = []; + var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function (range$$1) { + if (collapse) + { return dir < 0 ? range$$1.from() : range$$1.to() } + var headPos = cursorCoords(this$1, range$$1.head, "div"); + if (range$$1.goalColumn != null) { headPos.left = range$$1.goalColumn; } + goals.push(headPos.left); + var pos = findPosV(this$1, headPos, dir, unit); + if (unit == "page" && range$$1 == doc.sel.primary()) + { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top); } + return pos + }, sel_move); + if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) + { doc.sel.ranges[i].goalColumn = goals[i]; } } + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end; } + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function (ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } + : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); }; + while (start > 0 && check(line.charAt(start - 1))) { --start; } + while (end < line.length && check(line.charAt(end))) { ++end; } + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)) + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) { return } + if (this.state.overwrite = !this.state.overwrite) + { addClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + else + { rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt() }, + isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, + + scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y); }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)} + }, + + scrollIntoView: methodOp(function(range$$1, margin) { + if (range$$1 == null) { + range$$1 = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) { margin = this.options.cursorScrollMargin; } + } else if (typeof range$$1 == "number") { + range$$1 = {from: Pos(range$$1, 0), to: null}; + } else if (range$$1.from == null) { + range$$1 = {from: range$$1, to: null}; + } + if (!range$$1.to) { range$$1.to = range$$1.from; } + range$$1.margin = margin || 0; + + if (range$$1.from.line != null) { + scrollToRange(this, range$$1); + } else { + scrollToCoordsRange(this, range$$1.from, range$$1.to, range$$1.margin); + } + }), + + setSize: methodOp(function(width, height) { + var this$1 = this; + + var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; }; + if (width != null) { this.display.wrapper.style.width = interpret(width); } + if (height != null) { this.display.wrapper.style.height = interpret(height); } + if (this.options.lineWrapping) { clearLineMeasurementCache(this); } + var lineNo$$1 = this.display.viewFrom; + this.doc.iter(lineNo$$1, this.display.viewTo, function (line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) + { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo$$1, "widget"); break } } } + ++lineNo$$1; + }); + this.curOp.forceUpdate = true; + signal(this, "refresh", this); + }), + + operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + { estimateLineHeights(this); } + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + scrollToCoords(this, doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old + }), + + phrase: function(phraseText) { + var phrases = this.options.phrases; + return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText + }, + + getInputField: function(){return this.display.input.getField()}, + getWrapperElement: function(){return this.display.wrapper}, + getScrollerElement: function(){return this.display.scroller}, + getGutterElement: function(){return this.display.gutters} + }; + eventMixin(CodeMirror); + + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []}; } + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var oldPos = pos; + var origDir = dir; + var lineObj = getLine(doc, pos.line); + function findNextLine() { + var l = pos.line + dir; + if (l < doc.first || l >= doc.first + doc.size) { return false } + pos = new Pos(l, pos.ch, pos.sticky); + return lineObj = getLine(doc, l) + } + function moveOnce(boundToLine) { + var next; + if (visually) { + next = moveVisually(doc.cm, lineObj, pos, dir); + } else { + next = moveLogically(lineObj, pos, dir); + } + if (next == null) { + if (!boundToLine && findNextLine()) + { pos = endOfLine(visually, doc.cm, lineObj, pos.line, dir); } + else + { return false } + } else { + pos = next; + } + return true + } + + if (unit == "char") { + moveOnce(); + } else if (unit == "column") { + moveOnce(true); + } else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) { break } + var cur = lineObj.text.charAt(pos.ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) { type = "s"; } + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after";} + break + } + + if (type) { sawType = type; } + if (dir > 0 && !moveOnce(!first)) { break } + } + } + var result = skipAtomic(doc, pos, oldPos, origDir, true); + if (equalCursorPos(oldPos, result)) { result.hitSide = true; } + return result + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3); + y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount; + + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + var target; + for (;;) { + target = coordsChar(cm, x, y); + if (!target.outside) { break } + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } + y += dir * 5; + } + return target + } + + // CONTENTEDITABLE INPUT STYLE + + var ContentEditableInput = function(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.composing = null; + this.gracePeriod = false; + this.readDOMTimeout = null; + }; + + ContentEditableInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + disableBrowserMagic(div, cm.options.spellcheck); + + on(div, "paste", function (e) { + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + // IE doesn't fire input events, so we schedule a read for the pasted content in this way + if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20); } + }); + + on(div, "compositionstart", function (e) { + this$1.composing = {data: e.data, done: false}; + }); + on(div, "compositionupdate", function (e) { + if (!this$1.composing) { this$1.composing = {data: e.data, done: false}; } + }); + on(div, "compositionend", function (e) { + if (this$1.composing) { + if (e.data != this$1.composing.data) { this$1.readFromDOMSoon(); } + this$1.composing.done = true; + } + }); + + on(div, "touchstart", function () { return input.forceCompositionEnd(); }); + + on(div, "input", function () { + if (!this$1.composing) { this$1.readFromDOMSoon(); } + }); + + function onCopyCut(e) { + if (signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + if (e.type == "cut") { cm.replaceSelection("", null, "cut"); } + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.operation(function () { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + if (e.clipboardData) { + e.clipboardData.clearData(); + var content = lastCopied.text.join("\n"); + // iOS exposes the clipboard API, but seems to discard content inserted into it + e.clipboardData.setData("Text", content); + if (e.clipboardData.getData("Text") == content) { + e.preventDefault(); + return + } + } + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.text.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function () { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + if (hadFocus == div) { input.showPrimarySelection(); } + }, 50); + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }; + + ContentEditableInput.prototype.prepareSelection = function () { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result + }; + + ContentEditableInput.prototype.showSelection = function (info, takeFocus) { + if (!info || !this.cm.display.view.length) { return } + if (info.focus || takeFocus) { this.showPrimarySelection(); } + this.showMultipleSelections(info); + }; + + ContentEditableInput.prototype.getSelection = function () { + return this.cm.display.wrapper.ownerDocument.getSelection() + }; + + ContentEditableInput.prototype.showPrimarySelection = function () { + var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary(); + var from = prim.from(), to = prim.to(); + + if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { + sel.removeAllRanges(); + return + } + + var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), from) == 0 && + cmp(maxPos(curAnchor, curFocus), to) == 0) + { return } + + var view = cm.display.view; + var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || + {node: view[0].measure.map[2], offset: 0}; + var end = to.line < cm.display.viewTo && posToDOM(cm, to); + if (!end) { + var measure = view[view.length - 1].measure; + var map$$1 = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map$$1[map$$1.length - 1], offset: map$$1[map$$1.length - 2] - map$$1[map$$1.length - 3]}; + } + + if (!start || !end) { + sel.removeAllRanges(); + return + } + + var old = sel.rangeCount && sel.getRangeAt(0), rng; + try { rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + if (!gecko && cm.state.focused) { + sel.collapse(start.node, start.offset); + if (!rng.collapsed) { + sel.removeAllRanges(); + sel.addRange(rng); + } + } else { + sel.removeAllRanges(); + sel.addRange(rng); + } + if (old && sel.anchorNode == null) { sel.addRange(old); } + else if (gecko) { this.startGracePeriod(); } + } + this.rememberSelection(); + }; + + ContentEditableInput.prototype.startGracePeriod = function () { + var this$1 = this; + + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function () { + this$1.gracePeriod = false; + if (this$1.selectionChanged()) + { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }); } + }, 20); + }; + + ContentEditableInput.prototype.showMultipleSelections = function (info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }; + + ContentEditableInput.prototype.rememberSelection = function () { + var sel = this.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }; + + ContentEditableInput.prototype.selectionInEditor = function () { + var sel = this.getSelection(); + if (!sel.rangeCount) { return false } + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node) + }; + + ContentEditableInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor") { + if (!this.selectionInEditor()) + { this.showSelection(this.prepareSelection(), true); } + this.div.focus(); + } + }; + ContentEditableInput.prototype.blur = function () { this.div.blur(); }; + ContentEditableInput.prototype.getField = function () { return this.div }; + + ContentEditableInput.prototype.supportsTouch = function () { return true }; + + ContentEditableInput.prototype.receivedFocus = function () { + var input = this; + if (this.selectionInEditor()) + { this.pollSelection(); } + else + { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }); } + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }; + + ContentEditableInput.prototype.selectionChanged = function () { + var sel = this.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset + }; + + ContentEditableInput.prototype.pollSelection = function () { + if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } + var sel = this.getSelection(), cm = this.cm; + // On Android Chrome (version 56, at least), backspacing into an + // uneditable block element will put the cursor in that element, + // and then, because it's not editable, hide the virtual keyboard. + // Because Android doesn't allow us to actually detect backspace + // presses in a sane way, this code checks for when that happens + // and simulates a backspace press in this case. + if (android && chrome && this.cm.options.gutters.length && isInGutter(sel.anchorNode)) { + this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}); + this.blur(); + this.focus(); + return + } + if (this.composing) { return } + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) { runInOp(cm, function () { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true; } + }); } + }; + + ContentEditableInput.prototype.pollContent = function () { + if (this.readDOMTimeout != null) { + clearTimeout(this.readDOMTimeout); + this.readDOMTimeout = null; + } + + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.ch == 0 && from.line > cm.firstLine()) + { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length); } + if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) + { to = Pos(to.line + 1, 0); } + if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } + + var fromIndex, fromLine, fromNode; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + fromLine = lineNo(display.view[0].line); + fromNode = display.view[0].node; + } else { + fromLine = lineNo(display.view[fromIndex].line); + fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + var toLine, toNode; + if (toIndex == display.view.length - 1) { + toLine = display.viewTo - 1; + toNode = display.lineDiv.lastChild; + } else { + toLine = lineNo(display.view[toIndex + 1].line) - 1; + toNode = display.view[toIndex + 1].node.previousSibling; + } + + if (!fromNode) { return false } + var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else { break } + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + { ++cutFront; } + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + { ++cutEnd; } + // Try to move start of change to start of selection if ambiguous + if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { + while (cutFront && cutFront > from.ch && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { + cutFront--; + cutEnd++; + } + } + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, ""); + newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, ""); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true + } + }; + + ContentEditableInput.prototype.ensurePolled = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.reset = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.forceCompositionEnd = function () { + if (!this.composing) { return } + clearTimeout(this.readDOMTimeout); + this.composing = null; + this.updateFromDOM(); + this.div.blur(); + this.div.focus(); + }; + ContentEditableInput.prototype.readFromDOMSoon = function () { + var this$1 = this; + + if (this.readDOMTimeout != null) { return } + this.readDOMTimeout = setTimeout(function () { + this$1.readDOMTimeout = null; + if (this$1.composing) { + if (this$1.composing.done) { this$1.composing = null; } + else { return } + } + this$1.updateFromDOM(); + }, 80); + }; + + ContentEditableInput.prototype.updateFromDOM = function () { + var this$1 = this; + + if (this.cm.isReadOnly() || !this.pollContent()) + { runInOp(this.cm, function () { return regChange(this$1.cm); }); } + }; + + ContentEditableInput.prototype.setUneditable = function (node) { + node.contentEditable = "false"; + }; + + ContentEditableInput.prototype.onKeyPress = function (e) { + if (e.charCode == 0 || this.composing) { return } + e.preventDefault(); + if (!this.cm.isReadOnly()) + { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } + }; + + ContentEditableInput.prototype.readOnlyChanged = function (val) { + this.div.contentEditable = String(val != "nocursor"); + }; + + ContentEditableInput.prototype.onContextMenu = function () {}; + ContentEditableInput.prototype.resetPosition = function () {}; + + ContentEditableInput.prototype.needsContentAttribute = true; + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) { return null } + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line, cm.doc.direction), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); + result.offset = result.collapse == "right" ? result.end : result.start; + return result + } + + function isInGutter(node) { + for (var scan = node; scan; scan = scan.parentNode) + { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } + return false + } + + function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false; + function recognizeMarker(id) { return function (marker) { return marker.id == id; } } + function close() { + if (closing) { + text += lineSep; + if (extraLinebreak) { text += lineSep; } + closing = extraLinebreak = false; + } + } + function addText(str) { + if (str) { + close(); + text += str; + } + } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText) { + addText(cmText); + return + } + var markerID = node.getAttribute("cm-marker"), range$$1; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range$$1 = found[0].find(0))) + { addText(getBetween(cm.doc, range$$1.from, range$$1.to).join(lineSep)); } + return + } + if (node.getAttribute("contenteditable") == "false") { return } + var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName); + if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } + + if (isBlock) { close(); } + for (var i = 0; i < node.childNodes.length; i++) + { walk(node.childNodes[i]); } + + if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true; } + if (isBlock) { closing = true; } + } else if (node.nodeType == 3) { + addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")); + } + } + for (;;) { + walk(from); + if (from == to) { break } + from = from.nextSibling; + extraLinebreak = false; + } + return text + } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) { return null } + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + { return locateNodeInLineView(lineView, node, offset) } + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad) + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) { offset = textNode.nodeValue.length; } + } + while (topNode.parentNode != wrapper) { topNode = topNode.parentNode; } + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map$$1 = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map$$1.length; j += 3) { + var curNode = map$$1[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map$$1[j] + offset; + if (offset < 0 || curNode != textNode) { ch = map$$1[j + (offset ? 1 : 0)]; } + return Pos(line, ch) + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) { return badPos(found, bad) } + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + { return badPos(Pos(found.line, found.ch - dist), bad) } + else + { dist += after.textContent.length; } + } + for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + { return badPos(Pos(found.line, found.ch + dist$1), bad) } + else + { dist$1 += before.textContent.length; } + } + } + + // TEXTAREA INPUT STYLE + + var TextareaInput = function(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + this.composing = null; + }; + + TextareaInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = this.cm; + this.createField(display); + var te = this.textarea; + + display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) { te.style.width = "0px"; } + + on(te, "input", function () { + if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null; } + input.poll(); + }); + + on(te, "paste", function (e) { + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") { cm.state.cutIncoming = true; } + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function (e) { + if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } + cm.state.pasteIncoming = true; + input.focus(); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function (e) { + if (!eventInWidget(display, e)) { e_preventDefault(e); } + }); + + on(te, "compositionstart", function () { + var start = cm.getCursor("from"); + if (input.composing) { input.composing.range.clear(); } + input.composing = { + start: start, + range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) + }; + }); + on(te, "compositionend", function () { + if (input.composing) { + input.poll(); + input.composing.range.clear(); + input.composing = null; + } + }); + }; + + TextareaInput.prototype.createField = function (_display) { + // Wraps and hides input textarea + this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + this.textarea = this.wrapper.firstChild; + }; + + TextareaInput.prototype.prepareSelection = function () { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result + }; + + TextareaInput.prototype.showSelection = function (drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }; + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + TextareaInput.prototype.reset = function (typing) { + if (this.contextMenuPending || this.composing) { return } + var cm = this.cm; + if (cm.somethingSelected()) { + this.prevInput = ""; + var content = cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) { selectInput(this.textarea); } + if (ie && ie_version >= 9) { this.hasSelection = content; } + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) { this.hasSelection = null; } + } + }; + + TextareaInput.prototype.getField = function () { return this.textarea }; + + TextareaInput.prototype.supportsTouch = function () { return false }; + + TextareaInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }; + + TextareaInput.prototype.blur = function () { this.textarea.blur(); }; + + TextareaInput.prototype.resetPosition = function () { + this.wrapper.style.top = this.wrapper.style.left = 0; + }; + + TextareaInput.prototype.receivedFocus = function () { this.slowPoll(); }; + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + TextareaInput.prototype.slowPoll = function () { + var this$1 = this; + + if (this.pollingFast) { return } + this.polling.set(this.cm.options.pollInterval, function () { + this$1.poll(); + if (this$1.cm.state.focused) { this$1.slowPoll(); } + }); + }; + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + TextareaInput.prototype.fastPoll = function () { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }; + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + TextareaInput.prototype.poll = function () { + var this$1 = this; + + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (this.contextMenuPending || !cm.state.focused || + (hasSelection(input) && !prevInput && !this.composing) || + cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) + { return false } + + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) { return false } + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) { prevInput = "\u200b"; } + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same; } + + runInOp(cm, function () { + applyTextInput(cm, text.slice(same), prevInput.length - same, + null, this$1.composing ? "*compose" : null); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = ""; } + else { this$1.prevInput = text; } + + if (this$1.composing) { + this$1.composing.range.clear(); + this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), + {className: "CodeMirror-composing"}); + } + }); + return true + }; + + TextareaInput.prototype.ensurePolled = function () { + if (this.pollingFast && this.poll()) { this.pollingFast = false; } + }; + + TextareaInput.prototype.onKeyPress = function () { + if (ie && ie_version >= 9) { this.hasSelection = null; } + this.fastPoll(); + }; + + TextareaInput.prototype.onContextMenu = function (e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) { return } // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); } + + var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; + input.wrapper.style.cssText = "position: absolute"; + var wrapperBox = input.wrapper.getBoundingClientRect(); + te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + var oldScrollY; + if (webkit) { oldScrollY = window.scrollY; } // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) { window.scrollTo(null, oldScrollY); } + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) { te.value = input.prevInput = " "; } + input.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + input.contextMenuPending = false; + input.wrapper.style.cssText = oldWrapperCSS; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); } + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack(); } + var i = 0, poll = function () { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") { + operation(cm, selectAll)(cm); + } else if (i++ < 10) { + display.detectingSelectAll = setTimeout(poll, 500); + } else { + display.selForContextMenu = null; + display.input.reset(); + } + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) { prepareSelectAllHack(); } + if (captureRightClick) { + e_stop(e); + var mouseup = function () { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }; + + TextareaInput.prototype.readOnlyChanged = function (val) { + if (!val) { this.reset(); } + this.textarea.disabled = val == "nocursor"; + }; + + TextareaInput.prototype.setUneditable = function () {}; + + TextareaInput.prototype.needsContentAttribute = false; + + function fromTextArea(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + { options.tabindex = textarea.tabIndex; } + if (!options.placeholder && textarea.placeholder) + { options.placeholder = textarea.placeholder; } + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + + var realSubmit; + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form; + realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function () { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function (cm) { + cm.save = save; + cm.getTextArea = function () { return textarea; }; + cm.toTextArea = function () { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + { textarea.form.submit = realSubmit; } + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, + options); + return cm + } + + function addLegacyProps(CodeMirror) { + CodeMirror.off = off; + CodeMirror.on = on; + CodeMirror.wheelEventPixels = wheelEventPixels; + CodeMirror.Doc = Doc; + CodeMirror.splitLines = splitLinesAuto; + CodeMirror.countColumn = countColumn; + CodeMirror.findColumn = findColumn; + CodeMirror.isWordChar = isWordCharBasic; + CodeMirror.Pass = Pass; + CodeMirror.signal = signal; + CodeMirror.Line = Line; + CodeMirror.changeEnd = changeEnd; + CodeMirror.scrollbarModel = scrollbarModel; + CodeMirror.Pos = Pos; + CodeMirror.cmpPos = cmp; + CodeMirror.modes = modes; + CodeMirror.mimeModes = mimeModes; + CodeMirror.resolveMode = resolveMode; + CodeMirror.getMode = getMode; + CodeMirror.modeExtensions = modeExtensions; + CodeMirror.extendMode = extendMode; + CodeMirror.copyState = copyState; + CodeMirror.startState = startState; + CodeMirror.innerMode = innerMode; + CodeMirror.commands = commands; + CodeMirror.keyMap = keyMap; + CodeMirror.keyName = keyName; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.lookupKey = lookupKey; + CodeMirror.normalizeKeyMap = normalizeKeyMap; + CodeMirror.StringStream = StringStream; + CodeMirror.SharedTextMarker = SharedTextMarker; + CodeMirror.TextMarker = TextMarker; + CodeMirror.LineWidget = LineWidget; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + CodeMirror.e_stop = e_stop; + CodeMirror.addClass = addClass; + CodeMirror.contains = contains; + CodeMirror.rmClass = rmClass; + CodeMirror.keyNames = keyNames; + } + + // EDITOR CONSTRUCTOR + + defineOptions(CodeMirror); + + addEditorMethods(CodeMirror); + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); + for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + { CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments)} + })(Doc.prototype[prop]); } } + + eventMixin(Doc); + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name/*, mode, …*/) { + if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name; } + defineMode.apply(this, arguments); + }; + + CodeMirror.defineMIME = defineMIME; + + // Minimal default mode. + CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }); + CodeMirror.defineMIME("text/plain", "null"); + + // EXTENSIONS + + CodeMirror.defineExtension = function (name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function (name, func) { + Doc.prototype[name] = func; + }; + + CodeMirror.fromTextArea = fromTextArea; + + addLegacyProps(CodeMirror); + + CodeMirror.version = "5.41.0"; + + return CodeMirror; + +}))); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js new file mode 100644 index 0000000..77032ea --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js @@ -0,0 +1,879 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +function Context(indented, column, type, info, align, prev) { + this.indented = indented; + this.column = column; + this.type = type; + this.info = info; + this.align = align; + this.prev = prev; +} +function pushContext(state, col, type, info) { + var indent = state.indented; + if (state.context && state.context.type == "statement" && type != "statement") + indent = state.context.indented; + return state.context = new Context(indent, col, type, info, null, state.context); +} +function popContext(state) { + var t = state.context.type; + if (t == ")" || t == "]" || t == "}") + state.indented = state.context.indented; + return state.context = state.context.prev; +} + +function typeBefore(stream, state, pos) { + if (state.prevToken == "variable" || state.prevToken == "type") return true; + if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true; + if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true; +} + +function isTopScope(context) { + for (;;) { + if (!context || context.type == "top") return true; + if (context.type == "}" && context.prev.info != "namespace") return false; + context = context.prev; + } +} + +CodeMirror.defineMode("clike", function(config, parserConfig) { + var indentUnit = config.indentUnit, + statementIndentUnit = parserConfig.statementIndentUnit || indentUnit, + dontAlignCalls = parserConfig.dontAlignCalls, + keywords = parserConfig.keywords || {}, + types = parserConfig.types || {}, + builtin = parserConfig.builtin || {}, + blockKeywords = parserConfig.blockKeywords || {}, + defKeywords = parserConfig.defKeywords || {}, + atoms = parserConfig.atoms || {}, + hooks = parserConfig.hooks || {}, + multiLineStrings = parserConfig.multiLineStrings, + indentStatements = parserConfig.indentStatements !== false, + indentSwitch = parserConfig.indentSwitch !== false, + namespaceSeparator = parserConfig.namespaceSeparator, + isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/, + numberStart = parserConfig.numberStart || /[\d\.]/, + number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i, + isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/, + isIdentifierChar = parserConfig.isIdentifierChar || /[\w\$_\xa1-\uffff]/, + // An optional function that takes a {string} token and returns true if it + // should be treated as a builtin. + isReservedIdentifier = parserConfig.isReservedIdentifier || false; + + var curPunc, isDefKeyword; + + function tokenBase(stream, state) { + var ch = stream.next(); + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + if (isPunctuationChar.test(ch)) { + curPunc = ch; + return null; + } + if (numberStart.test(ch)) { + stream.backUp(1) + if (stream.match(number)) return "number" + stream.next() + } + if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } + if (stream.eat("/")) { + stream.skipToEnd(); + return "comment"; + } + } + if (isOperatorChar.test(ch)) { + while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {} + return "operator"; + } + stream.eatWhile(isIdentifierChar); + if (namespaceSeparator) while (stream.match(namespaceSeparator)) + stream.eatWhile(isIdentifierChar); + + var cur = stream.current(); + if (contains(keywords, cur)) { + if (contains(blockKeywords, cur)) curPunc = "newstatement"; + if (contains(defKeywords, cur)) isDefKeyword = true; + return "keyword"; + } + if (contains(types, cur)) return "type"; + if (contains(builtin, cur) + || (isReservedIdentifier && isReservedIdentifier(cur))) { + if (contains(blockKeywords, cur)) curPunc = "newstatement"; + return "builtin"; + } + if (contains(atoms, cur)) return "atom"; + return "variable"; + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next, end = false; + while ((next = stream.next()) != null) { + if (next == quote && !escaped) {end = true; break;} + escaped = !escaped && next == "\\"; + } + if (end || !(escaped || multiLineStrings)) + state.tokenize = null; + return "string"; + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return "comment"; + } + + function maybeEOL(stream, state) { + if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context)) + state.typeAtEndOfLine = typeBefore(stream, state, stream.pos) + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: null, + context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false), + indented: 0, + startOfLine: true, + prevToken: null + }; + }, + + token: function(stream, state) { + var ctx = state.context; + if (stream.sol()) { + if (ctx.align == null) ctx.align = false; + state.indented = stream.indentation(); + state.startOfLine = true; + } + if (stream.eatSpace()) { maybeEOL(stream, state); return null; } + curPunc = isDefKeyword = null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style == "comment" || style == "meta") return style; + if (ctx.align == null) ctx.align = true; + + if (curPunc == ";" || curPunc == ":" || (curPunc == "," && stream.match(/^\s*(?:\/\/.*)?$/, false))) + while (state.context.type == "statement") popContext(state); + else if (curPunc == "{") pushContext(state, stream.column(), "}"); + else if (curPunc == "[") pushContext(state, stream.column(), "]"); + else if (curPunc == "(") pushContext(state, stream.column(), ")"); + else if (curPunc == "}") { + while (ctx.type == "statement") ctx = popContext(state); + if (ctx.type == "}") ctx = popContext(state); + while (ctx.type == "statement") ctx = popContext(state); + } + else if (curPunc == ctx.type) popContext(state); + else if (indentStatements && + (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") || + (ctx.type == "statement" && curPunc == "newstatement"))) { + pushContext(state, stream.column(), "statement", stream.current()); + } + + if (style == "variable" && + ((state.prevToken == "def" || + (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) && + isTopScope(state.context) && stream.match(/^\s*\(/, false))))) + style = "def"; + + if (hooks.token) { + var result = hooks.token(stream, state, style); + if (result !== undefined) style = result; + } + + if (style == "def" && parserConfig.styleDefs === false) style = "variable"; + + state.startOfLine = false; + state.prevToken = isDefKeyword ? "def" : style || curPunc; + maybeEOL(stream, state); + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass; + var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); + var closing = firstChar == ctx.type; + if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev; + if (parserConfig.dontIndentStatements) + while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info)) + ctx = ctx.prev + if (hooks.indent) { + var hook = hooks.indent(state, ctx, textAfter, indentUnit); + if (typeof hook == "number") return hook + } + var switchBlock = ctx.prev && ctx.prev.info == "switch"; + if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) { + while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev + return ctx.indented + } + if (ctx.type == "statement") + return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit); + if (ctx.align && (!dontAlignCalls || ctx.type != ")")) + return ctx.column + (closing ? 0 : 1); + if (ctx.type == ")" && !closing) + return ctx.indented + statementIndentUnit; + + return ctx.indented + (closing ? 0 : indentUnit) + + (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0); + }, + + electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/, + blockCommentStart: "/*", + blockCommentEnd: "*/", + blockCommentContinue: " * ", + lineComment: "//", + fold: "brace" + }; +}); + + function words(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + function contains(words, word) { + if (typeof words === "function") { + return words(word); + } else { + return words.propertyIsEnumerable(word); + } + } + var cKeywords = "auto if break case register continue return default do sizeof " + + "static else struct switch extern typedef union for goto while enum const " + + "volatile inline restrict asm fortran"; + + // Do not use this. Use the cTypes function below. This is global just to avoid + // excessive calls when cTypes is being called multiple times during a parse. + var basicCTypes = words("int long char short double float unsigned signed " + + "void bool"); + + // Do not use this. Use the objCTypes function below. This is global just to avoid + // excessive calls when objCTypes is being called multiple times during a parse. + var basicObjCTypes = words("SEL instancetype id Class Protocol BOOL"); + + // Returns true if identifier is a "C" type. + // C type is defined as those that are reserved by the compiler (basicTypes), + // and those that end in _t (Reserved by POSIX for types) + // http://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html + function cTypes(identifier) { + return contains(basicCTypes, identifier) || /.+_t/.test(identifier); + } + + // Returns true if identifier is a "Objective C" type. + function objCTypes(identifier) { + return cTypes(identifier) || contains(basicObjCTypes, identifier); + } + + var cBlockKeywords = "case do else for if switch while struct enum union"; + var cDefKeywords = "struct enum union"; + + function cppHook(stream, state) { + if (!state.startOfLine) return false + for (var ch, next = null; ch = stream.peek();) { + if (ch == "\\" && stream.match(/^.$/)) { + next = cppHook + break + } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) { + break + } + stream.next() + } + state.tokenize = next + return "meta" + } + + function pointerHook(_stream, state) { + if (state.prevToken == "type") return "type"; + return false; + } + + // For C and C++ (and ObjC): identifiers starting with __ + // or _ followed by a capital letter are reserved for the compiler. + function cIsReservedIdentifier(token) { + if (!token || token.length < 2) return false; + if (token[0] != '_') return false; + return (token[1] == '_') || (token[1] !== token[1].toLowerCase()); + } + + function cpp14Literal(stream) { + stream.eatWhile(/[\w\.']/); + return "number"; + } + + function cpp11StringHook(stream, state) { + stream.backUp(1); + // Raw strings. + if (stream.match(/(R|u8R|uR|UR|LR)/)) { + var match = stream.match(/"([^\s\\()]{0,16})\(/); + if (!match) { + return false; + } + state.cpp11RawStringDelim = match[1]; + state.tokenize = tokenRawString; + return tokenRawString(stream, state); + } + // Unicode strings/chars. + if (stream.match(/(u8|u|U|L)/)) { + if (stream.match(/["']/, /* eat */ false)) { + return "string"; + } + return false; + } + // Ignore this hook. + stream.next(); + return false; + } + + function cppLooksLikeConstructor(word) { + var lastTwo = /(\w+)::~?(\w+)$/.exec(word); + return lastTwo && lastTwo[1] == lastTwo[2]; + } + + // C#-style strings where "" escapes a quote. + function tokenAtString(stream, state) { + var next; + while ((next = stream.next()) != null) { + if (next == '"' && !stream.eat('"')) { + state.tokenize = null; + break; + } + } + return "string"; + } + + // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where + // <delim> can be a string up to 16 characters long. + function tokenRawString(stream, state) { + // Escape characters that have special regex meanings. + var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&'); + var match = stream.match(new RegExp(".*?\\)" + delim + '"')); + if (match) + state.tokenize = null; + else + stream.skipToEnd(); + return "string"; + } + + function def(mimes, mode) { + if (typeof mimes == "string") mimes = [mimes]; + var words = []; + function add(obj) { + if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop)) + words.push(prop); + } + add(mode.keywords); + add(mode.types); + add(mode.builtin); + add(mode.atoms); + if (words.length) { + mode.helperType = mimes[0]; + CodeMirror.registerHelper("hintWords", mimes[0], words); + } + + for (var i = 0; i < mimes.length; ++i) + CodeMirror.defineMIME(mimes[i], mode); + } + + def(["text/x-csrc", "text/x-c", "text/x-chdr"], { + name: "clike", + keywords: words(cKeywords), + types: cTypes, + blockKeywords: words(cBlockKeywords), + defKeywords: words(cDefKeywords), + typeFirstDefinitions: true, + atoms: words("NULL true false"), + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + }, + modeProps: {fold: ["brace", "include"]} + }); + + def(["text/x-c++src", "text/x-c++hdr"], { + name: "clike", + keywords: words(cKeywords + " dynamic_cast namespace reinterpret_cast try explicit new " + + "static_cast typeid catch operator template typename class friend private " + + "this using const_cast public throw virtual delete mutable protected " + + "alignas alignof constexpr decltype nullptr noexcept thread_local final " + + "static_assert override"), + types: cTypes, + blockKeywords: words(cBlockKeywords +" class try catch finally"), + defKeywords: words(cDefKeywords + " class namespace"), + typeFirstDefinitions: true, + atoms: words("true false NULL"), + dontIndentStatements: /^template$/, + isIdentifierChar: /[\w\$_~\xa1-\uffff]/, + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + "u": cpp11StringHook, + "U": cpp11StringHook, + "L": cpp11StringHook, + "R": cpp11StringHook, + "0": cpp14Literal, + "1": cpp14Literal, + "2": cpp14Literal, + "3": cpp14Literal, + "4": cpp14Literal, + "5": cpp14Literal, + "6": cpp14Literal, + "7": cpp14Literal, + "8": cpp14Literal, + "9": cpp14Literal, + token: function(stream, state, style) { + if (style == "variable" && stream.peek() == "(" && + (state.prevToken == ";" || state.prevToken == null || + state.prevToken == "}") && + cppLooksLikeConstructor(stream.current())) + return "def"; + } + }, + namespaceSeparator: "::", + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-java", { + name: "clike", + keywords: words("abstract assert break case catch class const continue default " + + "do else enum extends final finally float for goto if implements import " + + "instanceof interface native new package private protected public " + + "return static strictfp super switch synchronized this throw throws transient " + + "try volatile while @interface"), + types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " + + "Integer Long Number Object Short String StringBuffer StringBuilder Void"), + blockKeywords: words("catch class do else finally for if switch try while"), + defKeywords: words("class interface enum @interface"), + typeFirstDefinitions: true, + atoms: words("true false null"), + number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i, + hooks: { + "@": function(stream) { + // Don't match the @interface keyword. + if (stream.match('interface', false)) return false; + + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + }, + modeProps: {fold: ["brace", "import"]} + }); + + def("text/x-csharp", { + name: "clike", + keywords: words("abstract as async await base break case catch checked class const continue" + + " default delegate do else enum event explicit extern finally fixed for" + + " foreach goto if implicit in interface internal is lock namespace new" + + " operator out override params private protected public readonly ref return sealed" + + " sizeof stackalloc static struct switch this throw try typeof unchecked" + + " unsafe using virtual void volatile while add alias ascending descending dynamic from get" + + " global group into join let orderby partial remove select set value var yield"), + types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" + + " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" + + " UInt64 bool byte char decimal double short int long object" + + " sbyte float string ushort uint ulong"), + blockKeywords: words("catch class do else finally for foreach if struct switch try while"), + defKeywords: words("class interface namespace struct var"), + typeFirstDefinitions: true, + atoms: words("true false null"), + hooks: { + "@": function(stream, state) { + if (stream.eat('"')) { + state.tokenize = tokenAtString; + return tokenAtString(stream, state); + } + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + } + }); + + function tokenTripleString(stream, state) { + var escaped = false; + while (!stream.eol()) { + if (!escaped && stream.match('"""')) { + state.tokenize = null; + break; + } + escaped = stream.next() == "\\" && !escaped; + } + return "string"; + } + + function tokenNestedComment(depth) { + return function (stream, state) { + var ch + while (ch = stream.next()) { + if (ch == "*" && stream.eat("/")) { + if (depth == 1) { + state.tokenize = null + break + } else { + state.tokenize = tokenNestedComment(depth - 1) + return state.tokenize(stream, state) + } + } else if (ch == "/" && stream.eat("*")) { + state.tokenize = tokenNestedComment(depth + 1) + return state.tokenize(stream, state) + } + } + return "comment" + } + } + + def("text/x-scala", { + name: "clike", + keywords: words( + /* scala */ + "abstract case catch class def do else extends final finally for forSome if " + + "implicit import lazy match new null object override package private protected return " + + "sealed super this throw trait try type val var while with yield _ " + + + /* package scala */ + "assert assume require print println printf readLine readBoolean readByte readShort " + + "readChar readInt readLong readFloat readDouble" + ), + types: words( + "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " + + "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " + + "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " + + "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " + + "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " + + + /* package java.lang */ + "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void" + ), + multiLineStrings: true, + blockKeywords: words("catch class enum do else finally for forSome if match switch try while"), + defKeywords: words("class enum def object package trait type val var"), + atoms: words("true false null"), + indentStatements: false, + indentSwitch: false, + isOperatorChar: /[+\-*&%=<>!?|\/#:@]/, + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '"': function(stream, state) { + if (!stream.match('""')) return false; + state.tokenize = tokenTripleString; + return state.tokenize(stream, state); + }, + "'": function(stream) { + stream.eatWhile(/[\w\$_\xa1-\uffff]/); + return "atom"; + }, + "=": function(stream, state) { + var cx = state.context + if (cx.type == "}" && cx.align && stream.eat(">")) { + state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev) + return "operator" + } else { + return false + } + }, + + "/": function(stream, state) { + if (!stream.eat("*")) return false + state.tokenize = tokenNestedComment(1) + return state.tokenize(stream, state) + } + }, + modeProps: {closeBrackets: {pairs: '()[]{}""', triples: '"'}} + }); + + function tokenKotlinString(tripleString){ + return function (stream, state) { + var escaped = false, next, end = false; + while (!stream.eol()) { + if (!tripleString && !escaped && stream.match('"') ) {end = true; break;} + if (tripleString && stream.match('"""')) {end = true; break;} + next = stream.next(); + if(!escaped && next == "$" && stream.match('{')) + stream.skipTo("}"); + escaped = !escaped && next == "\\" && !tripleString; + } + if (end || !tripleString) + state.tokenize = null; + return "string"; + } + } + + def("text/x-kotlin", { + name: "clike", + keywords: words( + /*keywords*/ + "package as typealias class interface this super val operator " + + "var fun for is in This throw return annotation " + + "break continue object if else while do try when !in !is as? " + + + /*soft keywords*/ + "file import where by get set abstract enum open inner override private public internal " + + "protected catch finally out final vararg reified dynamic companion constructor init " + + "sealed field property receiver param sparam lateinit data inline noinline tailrec " + + "external annotation crossinline const operator infix suspend actual expect setparam" + ), + types: words( + /* package java.lang */ + "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void Annotation Any BooleanArray " + + "ByteArray Char CharArray DeprecationLevel DoubleArray Enum FloatArray Function Int IntArray Lazy " + + "LazyThreadSafetyMode LongArray Nothing ShortArray Unit" + ), + intendSwitch: false, + indentStatements: false, + multiLineStrings: true, + number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+(\.\d+)?|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i, + blockKeywords: words("catch class do else finally for if where try while enum"), + defKeywords: words("class val var object interface fun"), + atoms: words("true false null this"), + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '"': function(stream, state) { + state.tokenize = tokenKotlinString(stream.match('""')); + return state.tokenize(stream, state); + }, + indent: function(state, ctx, textAfter, indentUnit) { + var firstChar = textAfter && textAfter.charAt(0); + if ((state.prevToken == "}" || state.prevToken == ")") && textAfter == "") + return state.indented; + if (state.prevToken == "operator" && textAfter != "}" || + state.prevToken == "variable" && firstChar == "." || + (state.prevToken == "}" || state.prevToken == ")") && firstChar == ".") + return indentUnit * 2 + ctx.indented; + if (ctx.align && ctx.type == "}") + return ctx.indented + (state.context.type == (textAfter || "").charAt(0) ? 0 : indentUnit); + } + }, + modeProps: {closeBrackets: {triples: '"'}} + }); + + def(["x-shader/x-vertex", "x-shader/x-fragment"], { + name: "clike", + keywords: words("sampler1D sampler2D sampler3D samplerCube " + + "sampler1DShadow sampler2DShadow " + + "const attribute uniform varying " + + "break continue discard return " + + "for while do if else struct " + + "in out inout"), + types: words("float int bool void " + + "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " + + "mat2 mat3 mat4"), + blockKeywords: words("for while do if else struct"), + builtin: words("radians degrees sin cos tan asin acos atan " + + "pow exp log exp2 sqrt inversesqrt " + + "abs sign floor ceil fract mod min max clamp mix step smoothstep " + + "length distance dot cross normalize ftransform faceforward " + + "reflect refract matrixCompMult " + + "lessThan lessThanEqual greaterThan greaterThanEqual " + + "equal notEqual any all not " + + "texture1D texture1DProj texture1DLod texture1DProjLod " + + "texture2D texture2DProj texture2DLod texture2DProjLod " + + "texture3D texture3DProj texture3DLod texture3DProjLod " + + "textureCube textureCubeLod " + + "shadow1D shadow2D shadow1DProj shadow2DProj " + + "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " + + "dFdx dFdy fwidth " + + "noise1 noise2 noise3 noise4"), + atoms: words("true false " + + "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " + + "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " + + "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " + + "gl_FogCoord gl_PointCoord " + + "gl_Position gl_PointSize gl_ClipVertex " + + "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " + + "gl_TexCoord gl_FogFragCoord " + + "gl_FragCoord gl_FrontFacing " + + "gl_FragData gl_FragDepth " + + "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " + + "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " + + "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " + + "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " + + "gl_ProjectionMatrixInverseTranspose " + + "gl_ModelViewProjectionMatrixInverseTranspose " + + "gl_TextureMatrixInverseTranspose " + + "gl_NormalScale gl_DepthRange gl_ClipPlane " + + "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " + + "gl_FrontLightModelProduct gl_BackLightModelProduct " + + "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " + + "gl_FogParameters " + + "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " + + "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " + + "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " + + "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " + + "gl_MaxDrawBuffers"), + indentSwitch: false, + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-nesc", { + name: "clike", + keywords: words(cKeywords + " as atomic async call command component components configuration event generic " + + "implementation includes interface module new norace nx_struct nx_union post provides " + + "signal task uses abstract extends"), + types: cTypes, + blockKeywords: words(cBlockKeywords), + atoms: words("null true false"), + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-objectivec", { + name: "clike", + keywords: words(cKeywords + " bycopy byref in inout oneway out self super atomic nonatomic retain copy " + + "readwrite readonly strong weak assign typeof nullable nonnull null_resettable _cmd " + + "@interface @implementation @end @protocol @encode @property @synthesize @dynamic @class " + + "@public @package @private @protected @required @optional @try @catch @finally @import " + + "@selector @encode @defs @synchronized @autoreleasepool @compatibility_alias @available"), + types: objCTypes, + builtin: words("FOUNDATION_EXPORT FOUNDATION_EXTERN NS_INLINE NS_FORMAT_FUNCTION NS_RETURNS_RETAINED " + + "NS_ERROR_ENUM NS_RETURNS_NOT_RETAINED NS_RETURNS_INNER_POINTER NS_DESIGNATED_INITIALIZER " + + "NS_ENUM NS_OPTIONS NS_REQUIRES_NIL_TERMINATION NS_ASSUME_NONNULL_BEGIN " + + "NS_ASSUME_NONNULL_END NS_SWIFT_NAME NS_REFINED_FOR_SWIFT"), + blockKeywords: words(cBlockKeywords + " @synthesize @try @catch @finally @autoreleasepool @synchronized"), + defKeywords: words(cDefKeywords + " @interface @implementation @protocol @class"), + dontIndentStatements: /^@.*$/, + typeFirstDefinitions: true, + atoms: words("YES NO NULL Nil nil true false nullptr"), + isReservedIdentifier: cIsReservedIdentifier, + hooks: { + "#": cppHook, + "*": pointerHook, + }, + modeProps: {fold: ["brace", "include"]} + }); + + def("text/x-squirrel", { + name: "clike", + keywords: words("base break clone continue const default delete enum extends function in class" + + " foreach local resume return this throw typeof yield constructor instanceof static"), + types: cTypes, + blockKeywords: words("case catch class else for foreach if switch try while"), + defKeywords: words("function local class"), + typeFirstDefinitions: true, + atoms: words("true false null"), + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + // Ceylon Strings need to deal with interpolation + var stringTokenizer = null; + function tokenCeylonString(type) { + return function(stream, state) { + var escaped = false, next, end = false; + while (!stream.eol()) { + if (!escaped && stream.match('"') && + (type == "single" || stream.match('""'))) { + end = true; + break; + } + if (!escaped && stream.match('``')) { + stringTokenizer = tokenCeylonString(type); + end = true; + break; + } + next = stream.next(); + escaped = type == "single" && !escaped && next == "\\"; + } + if (end) + state.tokenize = null; + return "string"; + } + } + + def("text/x-ceylon", { + name: "clike", + keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" + + " exists extends finally for function given if import in interface is let module new" + + " nonempty object of out outer package return satisfies super switch then this throw" + + " try value void while"), + types: function(word) { + // In Ceylon all identifiers that start with an uppercase are types + var first = word.charAt(0); + return (first === first.toUpperCase() && first !== first.toLowerCase()); + }, + blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"), + defKeywords: words("class dynamic function interface module object package value"), + builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" + + " native optional sealed see serializable shared suppressWarnings tagged throws variable"), + isPunctuationChar: /[\[\]{}\(\),;\:\.`]/, + isOperatorChar: /[+\-*&%=<>!?|^~:\/]/, + numberStart: /[\d#$]/, + number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i, + multiLineStrings: true, + typeFirstDefinitions: true, + atoms: words("true false null larger smaller equal empty finished"), + indentSwitch: false, + styleDefs: false, + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '"': function(stream, state) { + state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single"); + return state.tokenize(stream, state); + }, + '`': function(stream, state) { + if (!stringTokenizer || !stream.match('`')) return false; + state.tokenize = stringTokenizer; + stringTokenizer = null; + return state.tokenize(stream, state); + }, + "'": function(stream) { + stream.eatWhile(/[\w\$_\xa1-\uffff]/); + return "atom"; + }, + token: function(_stream, state, style) { + if ((style == "variable" || style == "type") && + state.prevToken == ".") { + return "variable-2"; + } + } + }, + modeProps: { + fold: ["brace", "import"], + closeBrackets: {triples: '"'} + } + }); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js new file mode 100644 index 0000000..b0a27ce --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js @@ -0,0 +1,899 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; + var isTS = parserConfig.typescript; + var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + return { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, + "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this"), "class": kw("class"), "super": kw("atom"), + "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, + "await": C + }; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^@]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; + + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) return; + if (next == "[") inSet = true; + else if (inSet && next == "]") inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) { + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (expressionAllowed(stream, state, 1)) { + readRegexp(stream); + stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); + return ret("regexp", "string-2"); + } else { + stream.eat("="); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#") { + stream.skipToEnd(); + return ret("error", "error"); + } else if (isOperatorChar.test(ch)) { + if (ch != ">" || !state.lexical || state.lexical.type != ">") { + if (stream.eat("=")) { + if (ch == "!" || ch == "=") stream.eat("=") + } else if (/[<>*+\-]/.test(ch)) { + stream.eat(ch) + if (ch == ">") stream.eat(ch) + } + } + return ret("operator", "operator", stream.current()); + } else if (wordRE.test(ch)) { + stream.eatWhile(wordRE); + var word = stream.current() + if (state.lastType != ".") { + if (keywords.propertyIsEnumerable(word)) { + var kw = keywords[word] + return ret(kw.type, kw.style, word) + } + if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) + return ret("async", "keyword", word) + } + return ret("variable", "variable", word) + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } + while ((next = stream.next()) != null) { + if (next == quote && !escaped) break; + escaped = !escaped && next == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + + var brackets = "([{}])"; + // This is a crude lookahead trick to try and notice that we're + // parsing the argument patterns for a fat-arrow function before we + // actually hit the arrow token. It only works if the arrow is on + // the same line as the arguments and there's no strange noise + // (comments) in between. Fallback is to only notice when we hit the + // arrow, and not declare the arguments as locals for the arrow + // body. + function findFatArrow(stream, state) { + if (state.fatArrowAt) state.fatArrowAt = null; + var arrow = stream.string.indexOf("=>", stream.start); + if (arrow < 0) return; + + if (isTS) { // Try to skip TypeScript return type declarations after the arguments + var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) + if (m) arrow = m.index + } + + var depth = 0, sawSomething = false; + for (var pos = arrow - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { ++pos; break; } + if (--depth == 0) { if (ch == "(") sawSomething = true; break; } + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (wordRE.test(ch)) { + sawSomething = true; + } else if (/["'\/]/.test(ch)) { + return; + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) state.fatArrowAt = pos; + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + for (var cx = state.context; cx; cx = cx.prev) { + for (var v = cx.vars; v; v = v.next) + if (v.name == varname) return true; + } + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function inList(name, list) { + for (var v = list; v; v = v.next) if (v.name == name) return true + return false; + } + function register(varname) { + var state = cx.state; + cx.marked = "def"; + if (state.context) { + if (state.lexical.info == "var" && state.context && state.context.block) { + // FIXME function decls are also not block scoped + var newContext = registerVarScoped(varname, state.context) + if (newContext != null) { + state.context = newContext + return + } + } else if (!inList(varname, state.localVars)) { + state.localVars = new Var(varname, state.localVars) + return + } + } + // Fall through means this is global + if (parserConfig.globalVars && !inList(varname, state.globalVars)) + state.globalVars = new Var(varname, state.globalVars) + } + function registerVarScoped(varname, context) { + if (!context) { + return null + } else if (context.block) { + var inner = registerVarScoped(varname, context.prev) + if (!inner) return null + if (inner == context.prev) return context + return new Context(inner, context.vars, true) + } else if (inList(varname, context.vars)) { + return context + } else { + return new Context(context.prev, new Var(varname, context.vars), false) + } + } + + function isModifier(name) { + return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" + } + + // Combinators + + function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } + function Var(name, next) { this.name = name; this.next = next } + + var defaultVars = new Var("this", new Var("arguments", null)) + function pushcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, false) + cx.state.localVars = defaultVars + } + function pushblockcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, true) + cx.state.localVars = null + } + function popcontext() { + cx.state.localVars = cx.state.context.vars + cx.state.context = cx.state.context.prev + } + popcontext.lex = true + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) + indent = outer.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + function exp(type) { + if (type == wanted) return cont(); + else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); + else return cont(exp); + }; + return exp; + } + + function statement(type, value) { + if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); + if (type == "debugger") return cont(expect(";")); + if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); + if (type == ";") return cont(); + if (type == "if") { + if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) + cx.state.cc.pop()(); + return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); + } + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); + if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), className, poplex); } + if (type == "variable") { + if (isTS && value == "declare") { + cx.marked = "keyword" + return cont(statement) + } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { + cx.marked = "keyword" + if (value == "enum") return cont(enumdef); + else if (value == "type") return cont(typeexpr, expect("operator"), typeexpr, expect(";")); + else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) + } else if (isTS && value == "namespace") { + cx.marked = "keyword" + return cont(pushlex("form"), expression, block, poplex) + } else if (isTS && value == "abstract") { + cx.marked = "keyword" + return cont(statement) + } else { + return cont(pushlex("stat"), maybelabel); + } + } + if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, + block, poplex, poplex, popcontext); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); + if (type == "export") return cont(pushlex("stat"), afterExport, poplex); + if (type == "import") return cont(pushlex("stat"), afterImport, poplex); + if (type == "async") return cont(statement) + if (value == "@") return cont(expression, statement) + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function maybeCatchBinding(type) { + if (type == "(") return cont(funarg, expect(")")) + } + function expression(type, value) { + return expressionInner(type, value, false); + } + function expressionNoComma(type, value) { + return expressionInner(type, value, true); + } + function parenExpr(type) { + if (type != "(") return pass() + return cont(pushlex(")"), expression, expect(")"), poplex) + } + function expressionInner(type, value, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); + else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef, maybeop); + if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } + if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); + if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); + if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type == "{") return contCommasep(objprop, "}", null, maybeop); + if (type == "quasi") return pass(quasi, maybeop); + if (type == "new") return cont(maybeTarget(noComma)); + if (type == "import") return cont(expression); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(expression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type == "operator") { + if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); + if (isTS && value == "<" && cx.stream.match(/^([^>]|<.*?>)*>\s*\(/, false)) + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == "quasi") { return pass(quasi, me); } + if (type == ";") return; + if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } + if (type == "regexp") { + cx.state.lastType = cx.marked = "operator" + cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) + return cont(expr) + } + } + function quasi(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasi); + return cont(expression, continueQuasi); + } + function continueQuasi(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasi); + } + } + function arrowBody(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expression); + } + function arrowBodyNoComma(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expressionNoComma); + } + function maybeTarget(noComma) { + return function(type) { + if (type == ".") return cont(noComma ? targetNoComma : target); + else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) + else return pass(noComma ? expressionNoComma : expression); + }; + } + function target(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } + } + function targetNoComma(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "async") { + cx.marked = "property"; + return cont(objprop); + } else if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params + if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) + cx.state.fatArrowAt = cx.stream.pos + m[0].length + return cont(afterprop); + } else if (type == "number" || type == "string") { + cx.marked = jsonldMode ? "property" : (cx.style + " property"); + return cont(afterprop); + } else if (type == "jsonld-keyword") { + return cont(afterprop); + } else if (isTS && isModifier(value)) { + cx.marked = "keyword" + return cont(objprop) + } else if (type == "[") { + return cont(expression, maybetype, expect("]"), afterprop); + } else if (type == "spread") { + return cont(expressionNoComma, afterprop); + } else if (value == "*") { + cx.marked = "keyword"; + return cont(objprop); + } else if (type == ":") { + return pass(afterprop) + } + } + function getterSetter(type) { + if (type != "variable") return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type) { + if (type == ":") return cont(expressionNoComma); + if (type == "(") return pass(functiondef); + } + function commasep(what, end, sep) { + function proceed(type, value) { + if (sep ? sep.indexOf(type) > -1 : type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(function(type, value) { + if (type == end || value == end) return pass() + return pass(what) + }, proceed); + } + if (type == end || value == end) return cont(); + return cont(expect(end)); + } + return function(type, value) { + if (type == end || value == end) return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end, info), commasep(what, end), poplex); + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type, value) { + if (isTS) { + if (type == ":") return cont(typeexpr); + if (value == "?") return cont(maybetype); + } + } + function mayberettype(type) { + if (isTS && type == ":") { + if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) + else return cont(typeexpr) + } + } + function isKW(_, value) { + if (value == "is") { + cx.marked = "keyword" + return cont() + } + } + function typeexpr(type, value) { + if (value == "keyof" || value == "typeof") { + cx.marked = "keyword" + return cont(value == "keyof" ? typeexpr : expressionNoComma) + } + if (type == "variable" || value == "void") { + cx.marked = "type" + return cont(afterType) + } + if (type == "string" || type == "number" || type == "atom") return cont(afterType); + if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) + if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) + if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType) + if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) + } + function maybeReturnType(type) { + if (type == "=>") return cont(typeexpr) + } + function typeprop(type, value) { + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property" + return cont(typeprop) + } else if (value == "?") { + return cont(typeprop) + } else if (type == ":") { + return cont(typeexpr) + } else if (type == "[") { + return cont(expression, maybetype, expect("]"), typeprop) + } + } + function typearg(type, value) { + if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) + if (type == ":") return cont(typeexpr) + return pass(typeexpr) + } + function afterType(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + if (value == "|" || type == "." || value == "&") return cont(typeexpr) + if (type == "[") return cont(expect("]"), afterType) + if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } + } + function maybeTypeArgs(_, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + } + function typeparam() { + return pass(typeexpr, maybeTypeDefault) + } + function maybeTypeDefault(_, value) { + if (value == "=") return cont(typeexpr) + } + function vardef(_, value) { + if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type, value) { + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } + if (type == "variable") { register(value); return cont(); } + if (type == "spread") return cont(pattern); + if (type == "[") return contCommasep(eltpattern, "]"); + if (type == "{") return contCommasep(proppattern, "}"); + } + function proppattern(type, value) { + if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type == "variable") cx.marked = "property"; + if (type == "spread") return cont(pattern); + if (type == "}") return pass(); + return cont(expect(":"), pattern, maybeAssign); + } + function eltpattern() { + return pass(pattern, maybeAssign) + } + function maybeAssign(_type, value) { + if (value == "=") return cont(expressionNoComma); + } + function vardefCont(type) { + if (type == ",") return cont(vardef); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); + } + function forspec(type, value) { + if (value == "await") return cont(forspec); + if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef, expect(";"), forspec2); + if (type == ";") return cont(forspec2); + if (type == "variable") return cont(formaybeinof); + return pass(expression, expect(";"), forspec2); + } + function formaybeinof(_type, value) { + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } + return cont(maybeoperatorComma, forspec2); + } + function forspec2(type, value) { + if (type == ";") return cont(forspec3); + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } + return pass(expression, expect(";"), forspec3); + } + function forspec3(type) { + if (type != ")") cont(expression); + } + function functiondef(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) + } + function funarg(type, value) { + if (value == "@") cont(expression, funarg) + if (type == "spread") return cont(funarg); + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } + return pass(pattern, maybetype, maybeAssign); + } + function classExpression(type, value) { + // Class expressions may have an optional name. + if (type == "variable") return className(type, value); + return classNameAfter(type, value); + } + function className(type, value) { + if (type == "variable") {register(value); return cont(classNameAfter);} + } + function classNameAfter(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) + if (value == "extends" || value == "implements" || (isTS && type == ",")) { + if (value == "implements") cx.marked = "keyword"; + return cont(isTS ? typeexpr : expression, classNameAfter); + } + if (type == "{") return cont(pushlex("}"), classBody, poplex); + } + function classBody(type, value) { + if (type == "async" || + (type == "variable" && + (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && + cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { + cx.marked = "keyword"; + return cont(classBody); + } + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + return cont(isTS ? classfield : functiondef, classBody); + } + if (type == "[") + return cont(expression, maybetype, expect("]"), isTS ? classfield : functiondef, classBody) + if (value == "*") { + cx.marked = "keyword"; + return cont(classBody); + } + if (type == ";") return cont(classBody); + if (type == "}") return cont(); + if (value == "@") return cont(expression, classBody) + } + function classfield(type, value) { + if (value == "?") return cont(classfield) + if (type == ":") return cont(typeexpr, maybeAssign) + if (value == "=") return cont(expressionNoComma) + return pass(functiondef) + } + function afterExport(type, value) { + if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } + if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } + if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); + return pass(statement); + } + function exportField(type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } + if (type == "variable") return pass(expressionNoComma, exportField); + } + function afterImport(type) { + if (type == "string") return cont(); + if (type == "(") return pass(expression); + return pass(importSpec, maybeMoreImports, maybeFrom); + } + function importSpec(type, value) { + if (type == "{") return contCommasep(importSpec, "}"); + if (type == "variable") register(value); + if (value == "*") cx.marked = "keyword"; + return cont(maybeAs); + } + function maybeMoreImports(type) { + if (type == ",") return cont(importSpec, maybeMoreImports) + } + function maybeAs(_type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } + } + function maybeFrom(_type, value) { + if (value == "from") { cx.marked = "keyword"; return cont(expression); } + } + function arrayLiteral(type) { + if (type == "]") return cont(); + return pass(commasep(expressionNoComma, "]")); + } + function enumdef() { + return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) + } + function enummember() { + return pass(pattern, maybeAssign); + } + + function isContinuedStatement(state, textAfter) { + return state.lastType == "operator" || state.lastType == "," || + isOperatorChar.test(textAfter.charAt(0)) || + /[,.]/.test(textAfter.charAt(0)); + } + + function expressionAllowed(stream, state, backUp) { + return state.tokenize == tokenBase && + /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || + (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) + } + + // Interface + + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && new Context(null, null, false), + indented: basecolumn || 0 + }; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; + return state; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == tokenComment) return CodeMirror.Pass; + if (state.tokenize != tokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top + // Kludge to prevent 'maybelse' from blocking lexical scope pops + if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse) break; + } + while ((lexical.type == "stat" || lexical.type == "form") && + (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && + (top == maybeoperatorComma || top == maybeoperatorNoComma) && + !/^[,\.=+\-*:?[\(]/.test(textAfter)))) + lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + blockCommentContinue: jsonMode ? null : " * ", + lineComment: jsonMode ? null : "//", + fold: "brace", + closeBrackets: "()[]{}''\"\"``", + + helperType: jsonMode ? "json" : "javascript", + jsonldMode: jsonldMode, + jsonMode: jsonMode, + + expressionAllowed: expressionAllowed, + + skipExpression: function(state) { + var top = state.cc[state.cc.length - 1] + if (top == expression || top == expressionNoComma) state.cc.pop() + } + }; +}); + +CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("text/ecmascript", "javascript"); +CodeMirror.defineMIME("application/javascript", "javascript"); +CodeMirror.defineMIME("application/x-javascript", "javascript"); +CodeMirror.defineMIME("application/ecmascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js new file mode 100644 index 0000000..80e2f20 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js @@ -0,0 +1,234 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../clike/clike")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../htmlmixed/htmlmixed", "../clike/clike"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function keywords(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + + // Helper for phpString + function matchSequence(list, end, escapes) { + if (list.length == 0) return phpString(end); + return function (stream, state) { + var patterns = list[0]; + for (var i = 0; i < patterns.length; i++) if (stream.match(patterns[i][0])) { + state.tokenize = matchSequence(list.slice(1), end); + return patterns[i][1]; + } + state.tokenize = phpString(end, escapes); + return "string"; + }; + } + function phpString(closing, escapes) { + return function(stream, state) { return phpString_(stream, state, closing, escapes); }; + } + function phpString_(stream, state, closing, escapes) { + // "Complex" syntax + if (escapes !== false && stream.match("${", false) || stream.match("{$", false)) { + state.tokenize = null; + return "string"; + } + + // Simple syntax + if (escapes !== false && stream.match(/^\$[a-zA-Z_][a-zA-Z0-9_]*/)) { + // After the variable name there may appear array or object operator. + if (stream.match("[", false)) { + // Match array operator + state.tokenize = matchSequence([ + [["[", null]], + [[/\d[\w\.]*/, "number"], + [/\$[a-zA-Z_][a-zA-Z0-9_]*/, "variable-2"], + [/[\w\$]+/, "variable"]], + [["]", null]] + ], closing, escapes); + } + if (stream.match(/\-\>\w/, false)) { + // Match object operator + state.tokenize = matchSequence([ + [["->", null]], + [[/[\w]+/, "variable"]] + ], closing, escapes); + } + return "variable-2"; + } + + var escaped = false; + // Normal string + while (!stream.eol() && + (escaped || escapes === false || + (!stream.match("{$", false) && + !stream.match(/^(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{)/, false)))) { + if (!escaped && stream.match(closing)) { + state.tokenize = null; + state.tokStack.pop(); state.tokStack.pop(); + break; + } + escaped = stream.next() == "\\" && !escaped; + } + return "string"; + } + + var phpKeywords = "abstract and array as break case catch class clone const continue declare default " + + "do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final " + + "for foreach function global goto if implements interface instanceof namespace " + + "new or private protected public static switch throw trait try use var while xor " + + "die echo empty exit eval include include_once isset list require require_once return " + + "print unset __halt_compiler self static parent yield insteadof finally"; + var phpAtoms = "true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__"; + var phpBuiltin = "func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists array_intersect_key array_combine array_column pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count"; + CodeMirror.registerHelper("hintWords", "php", [phpKeywords, phpAtoms, phpBuiltin].join(" ").split(" ")); + CodeMirror.registerHelper("wordChars", "php", /[\w$]/); + + var phpConfig = { + name: "clike", + helperType: "php", + keywords: keywords(phpKeywords), + blockKeywords: keywords("catch do else elseif for foreach if switch try while finally"), + defKeywords: keywords("class function interface namespace trait"), + atoms: keywords(phpAtoms), + builtin: keywords(phpBuiltin), + multiLineStrings: true, + hooks: { + "$": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "variable-2"; + }, + "<": function(stream, state) { + var before; + if (before = stream.match(/<<\s*/)) { + var quoted = stream.eat(/['"]/); + stream.eatWhile(/[\w\.]/); + var delim = stream.current().slice(before[0].length + (quoted ? 2 : 1)); + if (quoted) stream.eat(quoted); + if (delim) { + (state.tokStack || (state.tokStack = [])).push(delim, 0); + state.tokenize = phpString(delim, quoted != "'"); + return "string"; + } + } + return false; + }, + "#": function(stream) { + while (!stream.eol() && !stream.match("?>", false)) stream.next(); + return "comment"; + }, + "/": function(stream) { + if (stream.eat("/")) { + while (!stream.eol() && !stream.match("?>", false)) stream.next(); + return "comment"; + } + return false; + }, + '"': function(_stream, state) { + (state.tokStack || (state.tokStack = [])).push('"', 0); + state.tokenize = phpString('"'); + return "string"; + }, + "{": function(_stream, state) { + if (state.tokStack && state.tokStack.length) + state.tokStack[state.tokStack.length - 1]++; + return false; + }, + "}": function(_stream, state) { + if (state.tokStack && state.tokStack.length > 0 && + !--state.tokStack[state.tokStack.length - 1]) { + state.tokenize = phpString(state.tokStack[state.tokStack.length - 2]); + } + return false; + } + } + }; + + CodeMirror.defineMode("php", function(config, parserConfig) { + var htmlMode = CodeMirror.getMode(config, (parserConfig && parserConfig.htmlMode) || "text/html"); + var phpMode = CodeMirror.getMode(config, phpConfig); + + function dispatch(stream, state) { + var isPHP = state.curMode == phpMode; + if (stream.sol() && state.pending && state.pending != '"' && state.pending != "'") state.pending = null; + if (!isPHP) { + if (stream.match(/^<\?\w*/)) { + state.curMode = phpMode; + if (!state.php) state.php = CodeMirror.startState(phpMode, htmlMode.indent(state.html, "")) + state.curState = state.php; + return "meta"; + } + if (state.pending == '"' || state.pending == "'") { + while (!stream.eol() && stream.next() != state.pending) {} + var style = "string"; + } else if (state.pending && stream.pos < state.pending.end) { + stream.pos = state.pending.end; + var style = state.pending.style; + } else { + var style = htmlMode.token(stream, state.curState); + } + if (state.pending) state.pending = null; + var cur = stream.current(), openPHP = cur.search(/<\?/), m; + if (openPHP != -1) { + if (style == "string" && (m = cur.match(/[\'\"]$/)) && !/\?>/.test(cur)) state.pending = m[0]; + else state.pending = {end: stream.pos, style: style}; + stream.backUp(cur.length - openPHP); + } + return style; + } else if (isPHP && state.php.tokenize == null && stream.match("?>")) { + state.curMode = htmlMode; + state.curState = state.html; + if (!state.php.context.prev) state.php = null; + return "meta"; + } else { + return phpMode.token(stream, state.curState); + } + } + + return { + startState: function() { + var html = CodeMirror.startState(htmlMode) + var php = parserConfig.startOpen ? CodeMirror.startState(phpMode) : null + return {html: html, + php: php, + curMode: parserConfig.startOpen ? phpMode : htmlMode, + curState: parserConfig.startOpen ? php : html, + pending: null}; + }, + + copyState: function(state) { + var html = state.html, htmlNew = CodeMirror.copyState(htmlMode, html), + php = state.php, phpNew = php && CodeMirror.copyState(phpMode, php), cur; + if (state.curMode == htmlMode) cur = htmlNew; + else cur = phpNew; + return {html: htmlNew, php: phpNew, curMode: state.curMode, curState: cur, + pending: state.pending}; + }, + + token: dispatch, + + indent: function(state, textAfter) { + if ((state.curMode != phpMode && /^\s*<\//.test(textAfter)) || + (state.curMode == phpMode && /^\?>/.test(textAfter))) + return htmlMode.indent(state.html, textAfter); + return state.curMode.indent(state.curState, textAfter); + }, + + blockCommentStart: "/*", + blockCommentEnd: "*/", + lineComment: "//", + + innerMode: function(state) { return {state: state.curState, mode: state.curMode}; } + }; + }, "htmlmixed", "clike"); + + CodeMirror.defineMIME("application/x-httpd-php", "php"); + CodeMirror.defineMIME("application/x-httpd-php-open", {name: "php", startOpen: true}); + CodeMirror.defineMIME("text/x-php", phpConfig); +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js new file mode 100644 index 0000000..85f89b9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js @@ -0,0 +1,398 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + 'use strict'; + if (typeof exports == 'object' && typeof module == 'object') // CommonJS + mod(require('../../lib/codemirror')); + else if (typeof define == 'function' && define.amd) // AMD + define(['../../lib/codemirror'], mod); + else // Plain browser env + mod(window.CodeMirror); +})(function(CodeMirror) { +'use strict'; + +CodeMirror.defineMode('powershell', function() { + function buildRegexp(patterns, options) { + options = options || {}; + var prefix = options.prefix !== undefined ? options.prefix : '^'; + var suffix = options.suffix !== undefined ? options.suffix : '\\b'; + + for (var i = 0; i < patterns.length; i++) { + if (patterns[i] instanceof RegExp) { + patterns[i] = patterns[i].source; + } + else { + patterns[i] = patterns[i].replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + } + + return new RegExp(prefix + '(' + patterns.join('|') + ')' + suffix, 'i'); + } + + var notCharacterOrDash = '(?=[^A-Za-z\\d\\-_]|$)'; + var varNames = /[\w\-:]/ + var keywords = buildRegexp([ + /begin|break|catch|continue|data|default|do|dynamicparam/, + /else|elseif|end|exit|filter|finally|for|foreach|from|function|if|in/, + /param|process|return|switch|throw|trap|try|until|where|while/ + ], { suffix: notCharacterOrDash }); + + var punctuation = /[\[\]{},;`\.]|@[({]/; + var wordOperators = buildRegexp([ + 'f', + /b?not/, + /[ic]?split/, 'join', + /is(not)?/, 'as', + /[ic]?(eq|ne|[gl][te])/, + /[ic]?(not)?(like|match|contains)/, + /[ic]?replace/, + /b?(and|or|xor)/ + ], { prefix: '-' }); + var symbolOperators = /[+\-*\/%]=|\+\+|--|\.\.|[+\-*&^%:=!|\/]|<(?!#)|(?!#)>/; + var operators = buildRegexp([wordOperators, symbolOperators], { suffix: '' }); + + var numbers = /^((0x[\da-f]+)|((\d+\.\d+|\d\.|\.\d+|\d+)(e[\+\-]?\d+)?))[ld]?([kmgtp]b)?/i; + + var identifiers = /^[A-Za-z\_][A-Za-z\-\_\d]*\b/; + + var symbolBuiltins = /[A-Z]:|%|\?/i; + var namedBuiltins = buildRegexp([ + /Add-(Computer|Content|History|Member|PSSnapin|Type)/, + /Checkpoint-Computer/, + /Clear-(Content|EventLog|History|Host|Item(Property)?|Variable)/, + /Compare-Object/, + /Complete-Transaction/, + /Connect-PSSession/, + /ConvertFrom-(Csv|Json|SecureString|StringData)/, + /Convert-Path/, + /ConvertTo-(Csv|Html|Json|SecureString|Xml)/, + /Copy-Item(Property)?/, + /Debug-Process/, + /Disable-(ComputerRestore|PSBreakpoint|PSRemoting|PSSessionConfiguration)/, + /Disconnect-PSSession/, + /Enable-(ComputerRestore|PSBreakpoint|PSRemoting|PSSessionConfiguration)/, + /(Enter|Exit)-PSSession/, + /Export-(Alias|Clixml|Console|Counter|Csv|FormatData|ModuleMember|PSSession)/, + /ForEach-Object/, + /Format-(Custom|List|Table|Wide)/, + new RegExp('Get-(Acl|Alias|AuthenticodeSignature|ChildItem|Command|ComputerRestorePoint|Content|ControlPanelItem|Counter|Credential' + + '|Culture|Date|Event|EventLog|EventSubscriber|ExecutionPolicy|FormatData|Help|History|Host|HotFix|Item|ItemProperty|Job' + + '|Location|Member|Module|PfxCertificate|Process|PSBreakpoint|PSCallStack|PSDrive|PSProvider|PSSession|PSSessionConfiguration' + + '|PSSnapin|Random|Service|TraceSource|Transaction|TypeData|UICulture|Unique|Variable|Verb|WinEvent|WmiObject)'), + /Group-Object/, + /Import-(Alias|Clixml|Counter|Csv|LocalizedData|Module|PSSession)/, + /ImportSystemModules/, + /Invoke-(Command|Expression|History|Item|RestMethod|WebRequest|WmiMethod)/, + /Join-Path/, + /Limit-EventLog/, + /Measure-(Command|Object)/, + /Move-Item(Property)?/, + new RegExp('New-(Alias|Event|EventLog|Item(Property)?|Module|ModuleManifest|Object|PSDrive|PSSession|PSSessionConfigurationFile' + + '|PSSessionOption|PSTransportOption|Service|TimeSpan|Variable|WebServiceProxy|WinEvent)'), + /Out-(Default|File|GridView|Host|Null|Printer|String)/, + /Pause/, + /(Pop|Push)-Location/, + /Read-Host/, + /Receive-(Job|PSSession)/, + /Register-(EngineEvent|ObjectEvent|PSSessionConfiguration|WmiEvent)/, + /Remove-(Computer|Event|EventLog|Item(Property)?|Job|Module|PSBreakpoint|PSDrive|PSSession|PSSnapin|TypeData|Variable|WmiObject)/, + /Rename-(Computer|Item(Property)?)/, + /Reset-ComputerMachinePassword/, + /Resolve-Path/, + /Restart-(Computer|Service)/, + /Restore-Computer/, + /Resume-(Job|Service)/, + /Save-Help/, + /Select-(Object|String|Xml)/, + /Send-MailMessage/, + new RegExp('Set-(Acl|Alias|AuthenticodeSignature|Content|Date|ExecutionPolicy|Item(Property)?|Location|PSBreakpoint|PSDebug' + + '|PSSessionConfiguration|Service|StrictMode|TraceSource|Variable|WmiInstance)'), + /Show-(Command|ControlPanelItem|EventLog)/, + /Sort-Object/, + /Split-Path/, + /Start-(Job|Process|Service|Sleep|Transaction|Transcript)/, + /Stop-(Computer|Job|Process|Service|Transcript)/, + /Suspend-(Job|Service)/, + /TabExpansion2/, + /Tee-Object/, + /Test-(ComputerSecureChannel|Connection|ModuleManifest|Path|PSSessionConfigurationFile)/, + /Trace-Command/, + /Unblock-File/, + /Undo-Transaction/, + /Unregister-(Event|PSSessionConfiguration)/, + /Update-(FormatData|Help|List|TypeData)/, + /Use-Transaction/, + /Wait-(Event|Job|Process)/, + /Where-Object/, + /Write-(Debug|Error|EventLog|Host|Output|Progress|Verbose|Warning)/, + /cd|help|mkdir|more|oss|prompt/, + /ac|asnp|cat|cd|chdir|clc|clear|clhy|cli|clp|cls|clv|cnsn|compare|copy|cp|cpi|cpp|cvpa|dbp|del|diff|dir|dnsn|ebp/, + /echo|epal|epcsv|epsn|erase|etsn|exsn|fc|fl|foreach|ft|fw|gal|gbp|gc|gci|gcm|gcs|gdr|ghy|gi|gjb|gl|gm|gmo|gp|gps/, + /group|gsn|gsnp|gsv|gu|gv|gwmi|h|history|icm|iex|ihy|ii|ipal|ipcsv|ipmo|ipsn|irm|ise|iwmi|iwr|kill|lp|ls|man|md/, + /measure|mi|mount|move|mp|mv|nal|ndr|ni|nmo|npssc|nsn|nv|ogv|oh|popd|ps|pushd|pwd|r|rbp|rcjb|rcsn|rd|rdr|ren|ri/, + /rjb|rm|rmdir|rmo|rni|rnp|rp|rsn|rsnp|rujb|rv|rvpa|rwmi|sajb|sal|saps|sasv|sbp|sc|select|set|shcm|si|sl|sleep|sls/, + /sort|sp|spjb|spps|spsv|start|sujb|sv|swmi|tee|trcm|type|where|wjb|write/ + ], { prefix: '', suffix: '' }); + var variableBuiltins = buildRegexp([ + /[$?^_]|Args|ConfirmPreference|ConsoleFileName|DebugPreference|Error|ErrorActionPreference|ErrorView|ExecutionContext/, + /FormatEnumerationLimit|Home|Host|Input|MaximumAliasCount|MaximumDriveCount|MaximumErrorCount|MaximumFunctionCount/, + /MaximumHistoryCount|MaximumVariableCount|MyInvocation|NestedPromptLevel|OutputEncoding|Pid|Profile|ProgressPreference/, + /PSBoundParameters|PSCommandPath|PSCulture|PSDefaultParameterValues|PSEmailServer|PSHome|PSScriptRoot|PSSessionApplicationName/, + /PSSessionConfigurationName|PSSessionOption|PSUICulture|PSVersionTable|Pwd|ShellId|StackTrace|VerbosePreference/, + /WarningPreference|WhatIfPreference/, + + /Event|EventArgs|EventSubscriber|Sender/, + /Matches|Ofs|ForEach|LastExitCode|PSCmdlet|PSItem|PSSenderInfo|This/, + /true|false|null/ + ], { prefix: '\\$', suffix: '' }); + + var builtins = buildRegexp([symbolBuiltins, namedBuiltins, variableBuiltins], { suffix: notCharacterOrDash }); + + var grammar = { + keyword: keywords, + number: numbers, + operator: operators, + builtin: builtins, + punctuation: punctuation, + identifier: identifiers + }; + + // tokenizers + function tokenBase(stream, state) { + // Handle Comments + //var ch = stream.peek(); + + var parent = state.returnStack[state.returnStack.length - 1]; + if (parent && parent.shouldReturnFrom(state)) { + state.tokenize = parent.tokenize; + state.returnStack.pop(); + return state.tokenize(stream, state); + } + + if (stream.eatSpace()) { + return null; + } + + if (stream.eat('(')) { + state.bracketNesting += 1; + return 'punctuation'; + } + + if (stream.eat(')')) { + state.bracketNesting -= 1; + return 'punctuation'; + } + + for (var key in grammar) { + if (stream.match(grammar[key])) { + return key; + } + } + + var ch = stream.next(); + + // single-quote string + if (ch === "'") { + return tokenSingleQuoteString(stream, state); + } + + if (ch === '$') { + return tokenVariable(stream, state); + } + + // double-quote string + if (ch === '"') { + return tokenDoubleQuoteString(stream, state); + } + + if (ch === '<' && stream.eat('#')) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } + + if (ch === '#') { + stream.skipToEnd(); + return 'comment'; + } + + if (ch === '@') { + var quoteMatch = stream.eat(/["']/); + if (quoteMatch && stream.eol()) { + state.tokenize = tokenMultiString; + state.startQuote = quoteMatch[0]; + return tokenMultiString(stream, state); + } else if (stream.eol()) { + return 'error'; + } else if (stream.peek().match(/[({]/)) { + return 'punctuation'; + } else if (stream.peek().match(varNames)) { + // splatted variable + return tokenVariable(stream, state); + } + } + return 'error'; + } + + function tokenSingleQuoteString(stream, state) { + var ch; + while ((ch = stream.peek()) != null) { + stream.next(); + + if (ch === "'" && !stream.eat("'")) { + state.tokenize = tokenBase; + return 'string'; + } + } + + return 'error'; + } + + function tokenDoubleQuoteString(stream, state) { + var ch; + while ((ch = stream.peek()) != null) { + if (ch === '$') { + state.tokenize = tokenStringInterpolation; + return 'string'; + } + + stream.next(); + if (ch === '`') { + stream.next(); + continue; + } + + if (ch === '"' && !stream.eat('"')) { + state.tokenize = tokenBase; + return 'string'; + } + } + + return 'error'; + } + + function tokenStringInterpolation(stream, state) { + return tokenInterpolation(stream, state, tokenDoubleQuoteString); + } + + function tokenMultiStringReturn(stream, state) { + state.tokenize = tokenMultiString; + state.startQuote = '"' + return tokenMultiString(stream, state); + } + + function tokenHereStringInterpolation(stream, state) { + return tokenInterpolation(stream, state, tokenMultiStringReturn); + } + + function tokenInterpolation(stream, state, parentTokenize) { + if (stream.match('$(')) { + var savedBracketNesting = state.bracketNesting; + state.returnStack.push({ + /*jshint loopfunc:true */ + shouldReturnFrom: function(state) { + return state.bracketNesting === savedBracketNesting; + }, + tokenize: parentTokenize + }); + state.tokenize = tokenBase; + state.bracketNesting += 1; + return 'punctuation'; + } else { + stream.next(); + state.returnStack.push({ + shouldReturnFrom: function() { return true; }, + tokenize: parentTokenize + }); + state.tokenize = tokenVariable; + return state.tokenize(stream, state); + } + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == '>') { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch === '#'); + } + return 'comment'; + } + + function tokenVariable(stream, state) { + var ch = stream.peek(); + if (stream.eat('{')) { + state.tokenize = tokenVariableWithBraces; + return tokenVariableWithBraces(stream, state); + } else if (ch != undefined && ch.match(varNames)) { + stream.eatWhile(varNames); + state.tokenize = tokenBase; + return 'variable-2'; + } else { + state.tokenize = tokenBase; + return 'error'; + } + } + + function tokenVariableWithBraces(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch === '}') { + state.tokenize = tokenBase; + break; + } + } + return 'variable-2'; + } + + function tokenMultiString(stream, state) { + var quote = state.startQuote; + if (stream.sol() && stream.match(new RegExp(quote + '@'))) { + state.tokenize = tokenBase; + } + else if (quote === '"') { + while (!stream.eol()) { + var ch = stream.peek(); + if (ch === '$') { + state.tokenize = tokenHereStringInterpolation; + return 'string'; + } + + stream.next(); + if (ch === '`') { + stream.next(); + } + } + } + else { + stream.skipToEnd(); + } + + return 'string'; + } + + var external = { + startState: function() { + return { + returnStack: [], + bracketNesting: 0, + tokenize: tokenBase + }; + }, + + token: function(stream, state) { + return state.tokenize(stream, state); + }, + + blockCommentStart: '<#', + blockCommentEnd: '#>', + lineComment: '#', + fold: 'brace' + }; + return external; +}); + +CodeMirror.defineMIME('application/x-powershell', 'powershell'); +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js new file mode 100644 index 0000000..623c03f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js @@ -0,0 +1,409 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function wordRegexp(words) { + return new RegExp("^((" + words.join(")|(") + "))\\b"); + } + + var wordOperators = wordRegexp(["and", "or", "not", "is"]); + var commonKeywords = ["as", "assert", "break", "class", "continue", + "def", "del", "elif", "else", "except", "finally", + "for", "from", "global", "if", "import", + "lambda", "pass", "raise", "return", + "try", "while", "with", "yield", "in"]; + var commonBuiltins = ["abs", "all", "any", "bin", "bool", "bytearray", "callable", "chr", + "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod", + "enumerate", "eval", "filter", "float", "format", "frozenset", + "getattr", "globals", "hasattr", "hash", "help", "hex", "id", + "input", "int", "isinstance", "issubclass", "iter", "len", + "list", "locals", "map", "max", "memoryview", "min", "next", + "object", "oct", "open", "ord", "pow", "property", "range", + "repr", "reversed", "round", "set", "setattr", "slice", + "sorted", "staticmethod", "str", "sum", "super", "tuple", + "type", "vars", "zip", "__import__", "NotImplemented", + "Ellipsis", "__debug__"]; + CodeMirror.registerHelper("hintWords", "python", commonKeywords.concat(commonBuiltins)); + + function top(state) { + return state.scopes[state.scopes.length - 1]; + } + + CodeMirror.defineMode("python", function(conf, parserConf) { + var ERRORCLASS = "error"; + + var delimiters = parserConf.delimiters || parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.\\]/; + // (Backwards-compatiblity with old, cumbersome config system) + var operators = [parserConf.singleOperators, parserConf.doubleOperators, parserConf.doubleDelimiters, parserConf.tripleDelimiters, + parserConf.operators || /^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@])/] + for (var i = 0; i < operators.length; i++) if (!operators[i]) operators.splice(i--, 1) + + var hangingIndent = parserConf.hangingIndent || conf.indentUnit; + + var myKeywords = commonKeywords, myBuiltins = commonBuiltins; + if (parserConf.extra_keywords != undefined) + myKeywords = myKeywords.concat(parserConf.extra_keywords); + + if (parserConf.extra_builtins != undefined) + myBuiltins = myBuiltins.concat(parserConf.extra_builtins); + + var py3 = !(parserConf.version && Number(parserConf.version) < 3) + if (py3) { + // since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator + var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/; + myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]); + myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]); + var stringPrefixes = new RegExp("^(([rbuf]|(br)|(fr))?('{3}|\"{3}|['\"]))", "i"); + } else { + var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/; + myKeywords = myKeywords.concat(["exec", "print"]); + myBuiltins = myBuiltins.concat(["apply", "basestring", "buffer", "cmp", "coerce", "execfile", + "file", "intern", "long", "raw_input", "reduce", "reload", + "unichr", "unicode", "xrange", "False", "True", "None"]); + var stringPrefixes = new RegExp("^(([rubf]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i"); + } + var keywords = wordRegexp(myKeywords); + var builtins = wordRegexp(myBuiltins); + + // tokenizers + function tokenBase(stream, state) { + var sol = stream.sol() && state.lastToken != "\\" + if (sol) state.indent = stream.indentation() + // Handle scope changes + if (sol && top(state).type == "py") { + var scopeOffset = top(state).offset; + if (stream.eatSpace()) { + var lineOffset = stream.indentation(); + if (lineOffset > scopeOffset) + pushPyScope(state); + else if (lineOffset < scopeOffset && dedent(stream, state) && stream.peek() != "#") + state.errorToken = true; + return null; + } else { + var style = tokenBaseInner(stream, state); + if (scopeOffset > 0 && dedent(stream, state)) + style += " " + ERRORCLASS; + return style; + } + } + return tokenBaseInner(stream, state); + } + + function tokenBaseInner(stream, state) { + if (stream.eatSpace()) return null; + + // Handle Comments + if (stream.match(/^#.*/)) return "comment"; + + // Handle Number Literals + if (stream.match(/^[0-9\.]/, false)) { + var floatLiteral = false; + // Floats + if (stream.match(/^[\d_]*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; } + if (stream.match(/^[\d_]+\.\d*/)) { floatLiteral = true; } + if (stream.match(/^\.\d+/)) { floatLiteral = true; } + if (floatLiteral) { + // Float literals may be "imaginary" + stream.eat(/J/i); + return "number"; + } + // Integers + var intLiteral = false; + // Hex + if (stream.match(/^0x[0-9a-f_]+/i)) intLiteral = true; + // Binary + if (stream.match(/^0b[01_]+/i)) intLiteral = true; + // Octal + if (stream.match(/^0o[0-7_]+/i)) intLiteral = true; + // Decimal + if (stream.match(/^[1-9][\d_]*(e[\+\-]?[\d_]+)?/)) { + // Decimal literals may be "imaginary" + stream.eat(/J/i); + // TODO - Can you have imaginary longs? + intLiteral = true; + } + // Zero by itself with no other piece of number. + if (stream.match(/^0(?![\dx])/i)) intLiteral = true; + if (intLiteral) { + // Integer literals may be "long" + stream.eat(/L/i); + return "number"; + } + } + + // Handle Strings + if (stream.match(stringPrefixes)) { + var isFmtString = stream.current().toLowerCase().indexOf('f') !== -1; + if (!isFmtString) { + state.tokenize = tokenStringFactory(stream.current()); + return state.tokenize(stream, state); + } else { + state.tokenize = formatStringFactory(stream.current(), state.tokenize); + return state.tokenize(stream, state); + } + } + + for (var i = 0; i < operators.length; i++) + if (stream.match(operators[i])) return "operator" + + if (stream.match(delimiters)) return "punctuation"; + + if (state.lastToken == "." && stream.match(identifiers)) + return "property"; + + if (stream.match(keywords) || stream.match(wordOperators)) + return "keyword"; + + if (stream.match(builtins)) + return "builtin"; + + if (stream.match(/^(self|cls)\b/)) + return "variable-2"; + + if (stream.match(identifiers)) { + if (state.lastToken == "def" || state.lastToken == "class") + return "def"; + return "variable"; + } + + // Handle non-detected items + stream.next(); + return ERRORCLASS; + } + + function formatStringFactory(delimiter, tokenOuter) { + while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0) + delimiter = delimiter.substr(1); + + var singleline = delimiter.length == 1; + var OUTCLASS = "string"; + + function tokenFString(stream, state) { + // inside f-str Expression + if (stream.match(delimiter)) { + // expression ends pre-maturally, but very common in editing + // Could show error to remind users to close brace here + state.tokenize = tokenString + return OUTCLASS; + } else if (stream.match('{')) { + // starting brace, if not eaten below + return "punctuation"; + } else if (stream.match('}')) { + // return to regular inside string state + state.tokenize = tokenString + return "punctuation"; + } else { + // use tokenBaseInner to parse the expression + return tokenBaseInner(stream, state); + } + } + + function tokenString(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^'"\{\}\\]/); + if (stream.eat("\\")) { + stream.next(); + if (singleline && stream.eol()) + return OUTCLASS; + } else if (stream.match(delimiter)) { + state.tokenize = tokenOuter; + return OUTCLASS; + } else if (stream.match('{{')) { + // ignore {{ in f-str + return OUTCLASS; + } else if (stream.match('{', false)) { + // switch to nested mode + state.tokenize = tokenFString + if (stream.current()) { + return OUTCLASS; + } else { + // need to return something, so eat the starting { + stream.next(); + return "punctuation"; + } + } else if (stream.match('}}')) { + return OUTCLASS; + } else if (stream.match('}')) { + // single } in f-string is an error + return ERRORCLASS; + } else { + stream.eat(/['"]/); + } + } + if (singleline) { + if (parserConf.singleLineStringErrors) + return ERRORCLASS; + else + state.tokenize = tokenOuter; + } + return OUTCLASS; + } + tokenString.isString = true; + return tokenString; + } + + function tokenStringFactory(delimiter) { + while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0) + delimiter = delimiter.substr(1); + + var singleline = delimiter.length == 1; + var OUTCLASS = "string"; + + function tokenString(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^'"\\]/); + if (stream.eat("\\")) { + stream.next(); + if (singleline && stream.eol()) + return OUTCLASS; + } else if (stream.match(delimiter)) { + state.tokenize = tokenBase; + return OUTCLASS; + } else { + stream.eat(/['"]/); + } + } + if (singleline) { + if (parserConf.singleLineStringErrors) + return ERRORCLASS; + else + state.tokenize = tokenBase; + } + return OUTCLASS; + } + tokenString.isString = true; + return tokenString; + } + + function pushPyScope(state) { + while (top(state).type != "py") state.scopes.pop() + state.scopes.push({offset: top(state).offset + conf.indentUnit, + type: "py", + align: null}) + } + + function pushBracketScope(stream, state, type) { + var align = stream.match(/^([\s\[\{\(]|#.*)*$/, false) ? null : stream.column() + 1 + state.scopes.push({offset: state.indent + hangingIndent, + type: type, + align: align}) + } + + function dedent(stream, state) { + var indented = stream.indentation(); + while (state.scopes.length > 1 && top(state).offset > indented) { + if (top(state).type != "py") return true; + state.scopes.pop(); + } + return top(state).offset != indented; + } + + function tokenLexer(stream, state) { + if (stream.sol()) state.beginningOfLine = true; + + var style = state.tokenize(stream, state); + var current = stream.current(); + + // Handle decorators + if (state.beginningOfLine && current == "@") + return stream.match(identifiers, false) ? "meta" : py3 ? "operator" : ERRORCLASS; + + if (/\S/.test(current)) state.beginningOfLine = false; + + if ((style == "variable" || style == "builtin") + && state.lastToken == "meta") + style = "meta"; + + // Handle scope changes. + if (current == "pass" || current == "return") + state.dedent += 1; + + if (current == "lambda") state.lambda = true; + if (current == ":" && !state.lambda && top(state).type == "py") + pushPyScope(state); + + if (current.length == 1 && !/string|comment/.test(style)) { + var delimiter_index = "[({".indexOf(current); + if (delimiter_index != -1) + pushBracketScope(stream, state, "])}".slice(delimiter_index, delimiter_index+1)); + + delimiter_index = "])}".indexOf(current); + if (delimiter_index != -1) { + if (top(state).type == current) state.indent = state.scopes.pop().offset - hangingIndent + else return ERRORCLASS; + } + } + if (state.dedent > 0 && stream.eol() && top(state).type == "py") { + if (state.scopes.length > 1) state.scopes.pop(); + state.dedent -= 1; + } + + return style; + } + + var external = { + startState: function(basecolumn) { + return { + tokenize: tokenBase, + scopes: [{offset: basecolumn || 0, type: "py", align: null}], + indent: basecolumn || 0, + lastToken: null, + lambda: false, + dedent: 0 + }; + }, + + token: function(stream, state) { + var addErr = state.errorToken; + if (addErr) state.errorToken = false; + var style = tokenLexer(stream, state); + + if (style && style != "comment") + state.lastToken = (style == "keyword" || style == "punctuation") ? stream.current() : style; + if (style == "punctuation") style = null; + + if (stream.eol() && state.lambda) + state.lambda = false; + return addErr ? style + " " + ERRORCLASS : style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase) + return state.tokenize.isString ? CodeMirror.Pass : 0; + + var scope = top(state), closing = scope.type == textAfter.charAt(0) + if (scope.align != null) + return scope.align - (closing ? 1 : 0) + else + return scope.offset - (closing ? hangingIndent : 0) + }, + + electricInput: /^\s*[\}\]\)]$/, + closeBrackets: {triples: "'\""}, + lineComment: "#", + fold: "indent" + }; + return external; + }); + + CodeMirror.defineMIME("text/x-python", "python"); + + var words = function(str) { return str.split(" "); }; + + CodeMirror.defineMIME("text/x-cython", { + name: "python", + extra_keywords: words("by cdef cimport cpdef ctypedef enum except "+ + "extern gil include nogil property public "+ + "readonly struct union DEF IF ELIF ELSE") + }); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js new file mode 100644 index 0000000..5af1241 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode('shell', function() { + + var words = {}; + function define(style, dict) { + for(var i = 0; i < dict.length; i++) { + words[dict[i]] = style; + } + }; + + var commonAtoms = ["true", "false"]; + var commonKeywords = ["if", "then", "do", "else", "elif", "while", "until", "for", "in", "esac", "fi", + "fin", "fil", "done", "exit", "set", "unset", "export", "function"]; + var commonCommands = ["ab", "awk", "bash", "beep", "cat", "cc", "cd", "chown", "chmod", "chroot", "clear", + "cp", "curl", "cut", "diff", "echo", "find", "gawk", "gcc", "get", "git", "grep", "hg", "kill", "killall", + "ln", "ls", "make", "mkdir", "openssl", "mv", "nc", "nl", "node", "npm", "ping", "ps", "restart", "rm", + "rmdir", "sed", "service", "sh", "shopt", "shred", "source", "sort", "sleep", "ssh", "start", "stop", + "su", "sudo", "svn", "tee", "telnet", "top", "touch", "vi", "vim", "wall", "wc", "wget", "who", "write", + "yes", "zsh"]; + + CodeMirror.registerHelper("hintWords", "shell", commonAtoms.concat(commonKeywords, commonCommands)); + + define('atom', commonAtoms); + define('keyword', commonKeywords); + define('builtin', commonCommands); + + function tokenBase(stream, state) { + if (stream.eatSpace()) return null; + + var sol = stream.sol(); + var ch = stream.next(); + + if (ch === '\\') { + stream.next(); + return null; + } + if (ch === '\'' || ch === '"' || ch === '`') { + state.tokens.unshift(tokenString(ch, ch === "`" ? "quote" : "string")); + return tokenize(stream, state); + } + if (ch === '#') { + if (sol && stream.eat('!')) { + stream.skipToEnd(); + return 'meta'; // 'comment'? + } + stream.skipToEnd(); + return 'comment'; + } + if (ch === '$') { + state.tokens.unshift(tokenDollar); + return tokenize(stream, state); + } + if (ch === '+' || ch === '=') { + return 'operator'; + } + if (ch === '-') { + stream.eat('-'); + stream.eatWhile(/\w/); + return 'attribute'; + } + if (/\d/.test(ch)) { + stream.eatWhile(/\d/); + if(stream.eol() || !/\w/.test(stream.peek())) { + return 'number'; + } + } + stream.eatWhile(/[\w-]/); + var cur = stream.current(); + if (stream.peek() === '=' && /\w+/.test(cur)) return 'def'; + return words.hasOwnProperty(cur) ? words[cur] : null; + } + + function tokenString(quote, style) { + var close = quote == "(" ? ")" : quote == "{" ? "}" : quote + return function(stream, state) { + var next, escaped = false; + while ((next = stream.next()) != null) { + if (next === close && !escaped) { + state.tokens.shift(); + break; + } else if (next === '$' && !escaped && quote !== "'" && stream.peek() != close) { + escaped = true; + stream.backUp(1); + state.tokens.unshift(tokenDollar); + break; + } else if (!escaped && quote !== close && next === quote) { + state.tokens.unshift(tokenString(quote, style)) + return tokenize(stream, state) + } else if (!escaped && /['"]/.test(next) && !/['"]/.test(quote)) { + state.tokens.unshift(tokenStringStart(next, "string")); + stream.backUp(1); + break; + } + escaped = !escaped && next === '\\'; + } + return style; + }; + }; + + function tokenStringStart(quote, style) { + return function(stream, state) { + state.tokens[0] = tokenString(quote, style) + stream.next() + return tokenize(stream, state) + } + } + + var tokenDollar = function(stream, state) { + if (state.tokens.length > 1) stream.eat('$'); + var ch = stream.next() + if (/['"({]/.test(ch)) { + state.tokens[0] = tokenString(ch, ch == "(" ? "quote" : ch == "{" ? "def" : "string"); + return tokenize(stream, state); + } + if (!/\d/.test(ch)) stream.eatWhile(/\w/); + state.tokens.shift(); + return 'def'; + }; + + function tokenize(stream, state) { + return (state.tokens[0] || tokenBase) (stream, state); + }; + + return { + startState: function() {return {tokens:[]};}, + token: function(stream, state) { + return tokenize(stream, state); + }, + closeBrackets: "()[]{}''\"\"``", + lineComment: '#', + fold: "brace" + }; +}); + +CodeMirror.defineMIME('text/x-sh', 'shell'); +// Apache uses a slightly different Media Type for Shell scripts +// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types +CodeMirror.defineMIME('application/x-sh', 'shell'); + +}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js new file mode 100644 index 0000000..2239372 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js @@ -0,0 +1,1106 @@ +(function ($) { + // var resultsName = ""; + var inputElement; + var displayElement; + $.fn.extend({ + cronGen: function (options) { + if (options == null) { + options = {}; + } + options = $.extend({}, $.fn.cronGen.defaultOptions, options); + //create top menu + var cronContainer = $("<div/>", { id: "CronContainer", style: "display:none;width:300px;height:300px;" }); + var mainDiv = $("<div/>", { id: "CronGenMainDiv", style: "width:410px;height:420px;" }); + var topMenu = $("<ul/>", { "class": "nav nav-tabs", id: "CronGenTabs" }); + $('<li/>', { 'class': 'active' }).html($('<a id="SecondlyTab" href="#Secondly">秒</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="MinutesTab" href="#Minutes">分钟</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="HourlyTab" href="#Hourly">小时</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="DailyTab" href="#Daily">日</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="MonthlyTab" href="#Monthly">月</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="WeeklyTab" href="#Weekly">周</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="YearlyTab" href="#Yearly">年</a>')).appendTo(topMenu); + $(topMenu).appendTo(mainDiv); + + //create what's inside the tabs + var container = $("<div/>", { "class": "container-fluid", "style": "margin-top: 30px;margin-left: -14px;" }); + var row = $("<div/>", { "class": "row-fluid" }); + var span12 = $("<div/>", { "class": "span12" }); + var tabContent = $("<div/>", { "class": "tab-content", "style": "border:0px; margin-top:-20px;" }); + + + //creating the secondsTab + var secondsTab = $("<div/>", { "class": "tab-pane active", id: "Secondly" }); + var seconds1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "second"}).appendTo(seconds1); + $(seconds1).append("每秒 允许的通配符[, - * /]"); + $(seconds1).appendTo(secondsTab); + + var seconds2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "second"}).appendTo(seconds2); + $(seconds2).append("周期 从"); + $("<input/>",{type : "text", id : "secondStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds2); + $(seconds2).append("-"); + $("<input/>",{type : "text", id : "secondEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds2); + $(seconds2).append("秒"); + $(seconds2).appendTo(secondsTab); + + var seconds3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "second"}).appendTo(seconds3); + $(seconds3).append("从"); + $("<input/>",{type : "text", id : "secondStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds3); + $(seconds3).append("秒开始,每"); + $("<input/>",{type : "text", id : "secondEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds3); + $(seconds3).append("秒执行一次"); + $(seconds3).appendTo(secondsTab); + + var seconds4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "second", id: "sencond_appoint"}).appendTo(seconds4); + $(seconds4).append("指定"); + $(seconds4).appendTo(secondsTab); + + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="0">00<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="24">24<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="25">25<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="26">26<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="27">27<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="28">28<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="29">29</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="30">30<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="31">31<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="32">32<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="33">33<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="34">34<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="35">35<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="36">36<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="37">37<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="38">38<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="39">39</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="40">40<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="41">41<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="42">42<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="43">43<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="44">44<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="45">45<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="46">46<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="47">47<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="48">48<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="49">49</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="50">50<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="51">51<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="52">52<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="53">53<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="54">54<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="55">55<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="56">56<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="57">57<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="58">58<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="59">59</div>'); + $("<input/>",{type : "hidden", id : "secondHidden"}).appendTo(secondsTab); + $(secondsTab).appendTo(tabContent); + + //creating the minutesTab + var minutesTab = $("<div/>", { "class": "tab-pane", id: "Minutes" }); + + var minutes1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "min"}).appendTo(minutes1); + $(minutes1).append("每分钟 允许的通配符[, - * /]"); + $(minutes1).appendTo(minutesTab); + + var minutes2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "min"}).appendTo(minutes2); + $(minutes2).append("周期 从"); + $("<input/>",{type : "text", id : "minStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes2); + $(minutes2).append("-"); + $("<input/>",{type : "text", id : "minEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes2); + $(minutes2).append("分钟"); + $(minutes2).appendTo(minutesTab); + + var minutes3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "min"}).appendTo(minutes3); + $(minutes3).append("从"); + $("<input/>",{type : "text", id : "minStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes3); + $(minutes3).append("分钟开始,每"); + $("<input/>",{type : "text", id : "minEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes3); + $(minutes3).append("分钟执行一次"); + $(minutes3).appendTo(minutesTab); + + var minutes4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "min", id: "min_appoint"}).appendTo(minutes4); + $(minutes4).append("指定"); + $(minutes4).appendTo(minutesTab); + + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="0">00<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="24">24<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="25">25<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="26">26<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="27">27<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="28">28<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="29">29</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="30">30<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="31">31<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="32">32<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="33">33<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="34">34<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="35">35<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="36">36<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="37">37<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="38">38<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="39">39</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="40">40<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="41">41<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="42">42<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="43">43<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="44">44<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="45">45<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="46">46<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="47">47<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="48">48<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="49">49</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="50">50<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="51">51<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="52">52<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="53">53<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="54">54<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="55">55<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="56">56<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="57">57<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="58">58<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="59">59</div>'); + $("<input/>",{type : "hidden", id : "minHidden"}).appendTo(minutesTab); + $(minutesTab).appendTo(tabContent); + + //creating the hourlyTab + var hourlyTab = $("<div/>", { "class": "tab-pane", id: "Hourly" }); + + var hourly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "hour"}).appendTo(hourly1); + $(hourly1).append("每小时 允许的通配符[, - * /]"); + $(hourly1).appendTo(hourlyTab); + + var hourly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "hour"}).appendTo(hourly2); + $(hourly2).append("周期 从"); + $("<input/>",{type : "text", id : "hourStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly2); + $(hourly2).append("-"); + $("<input/>",{type : "text", id : "hourEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly2); + $(hourly2).append("小时"); + $(hourly2).appendTo(hourlyTab); + + var hourly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "hour"}).appendTo(hourly3); + $(hourly3).append("从"); + $("<input/>",{type : "text", id : "hourStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly3); + $(hourly3).append("小时开始,每"); + $("<input/>",{type : "text", id : "hourEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly3); + $(hourly3).append("小时执行一次"); + $(hourly3).appendTo(hourlyTab); + + var hourly4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "hour", id: "hour_appoint"}).appendTo(hourly4); + $(hourly4).append("指定"); + $(hourly4).appendTo(hourlyTab); + + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="0">00<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05</div>'); + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11</div>'); + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17</div>'); + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23</div>'); + $("<input/>",{type : "hidden", id : "hourHidden"}).appendTo(hourlyTab); + $(hourlyTab).appendTo(tabContent); + + + //creating the dailyTab + var dailyTab = $("<div/>", { "class": "tab-pane", id: "Daily" }); + + var daily1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "day"}).appendTo(daily1); + $(daily1).append("每天 允许的通配符[, - * / L W]"); + $(daily1).appendTo(dailyTab); + + var daily5 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "day"}).appendTo(daily5); + $(daily5).append("不指定"); + $(daily5).appendTo(dailyTab); + + var daily2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "day"}).appendTo(daily2); + $(daily2).append("周期 从"); + $("<input/>",{type : "text", id : "dayStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily2); + $(daily2).append("-"); + $("<input/>",{type : "text", id : "dayEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily2); + $(daily2).append("日"); + $(daily2).appendTo(dailyTab); + + var daily3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "day"}).appendTo(daily3); + $(daily3).append("从"); + $("<input/>",{type : "text", id : "dayStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily3); + $(daily3).append("日开始,每"); + $("<input/>",{type : "text", id : "dayEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily3); + $(daily3).append("天执行一次"); + $(daily3).appendTo(dailyTab); + + var daily6 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "5", name : "day"}).appendTo(daily6); + $(daily6).append("每月"); + $("<input/>",{type : "text", id : "dayStart_2", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily6); + $(daily6).append("号最近的那个工作日"); + $(daily6).appendTo(dailyTab); + + var daily7 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "6", name : "day"}).appendTo(daily7); + $(daily7).append("本月最后一天"); + $(daily7).appendTo(dailyTab); + + var daily4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "7", name : "day", id: "day_appoint"}).appendTo(daily4); + $(daily4).append("指定"); + $(daily4).appendTo(dailyTab); + + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10</div>'); + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20</div>'); + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="24">24<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="25">25<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="26">26<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="27">27<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="28">28<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="29">29<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="30">30</div>'); + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="31">31</div>'); + $("<input/>",{type : "hidden", id : "dayHidden"}).appendTo(dailyTab); + $(dailyTab).appendTo(tabContent); + + + //creating the monthlyTab + var monthlyTab = $("<div/>", { "class": "tab-pane", id: "Monthly" }); + + var monthly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "month"}).appendTo(monthly1); + $(monthly1).append("每月 允许的通配符[, - * /]"); + $(monthly1).appendTo(monthlyTab); + + var monthly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "month"}).appendTo(monthly2); + $(monthly2).append("不指定"); + $(monthly2).appendTo(monthlyTab); + + var monthly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "month"}).appendTo(monthly3); + $(monthly3).append("周期 从"); + $("<input/>",{type : "text", id : "monthStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly3); + $(monthly3).append("-"); + $("<input/>",{type : "text", id : "monthEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly3); + $(monthly3).append("月"); + $(monthly3).appendTo(monthlyTab); + + var monthly4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "month"}).appendTo(monthly4); + $(monthly4).append("从"); + $("<input/>",{type : "text", id : "monthStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly4); + $(monthly4).append("月开始,每"); + $("<input/>",{type : "text", id : "monthEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly4); + $(monthly4).append("月执行一次"); + $(monthly4).appendTo(monthlyTab); + + var monthly5 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "5", name : "month", id: "month_appoint"}).appendTo(monthly5); + $(monthly5).append("指定"); + $(monthly5).appendTo(monthlyTab); + + $(monthlyTab).append('<div class="imp monthList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06</div>'); + $(monthlyTab).append('<div class="imp monthList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12</div>'); + $("<input/>",{type : "hidden", id : "monthHidden"}).appendTo(monthlyTab); + $(monthlyTab).appendTo(tabContent); + + //creating the weeklyTab + var weeklyTab = $("<div/>", { "class": "tab-pane", id: "Weekly" }); + + var weekly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "week"}).appendTo(weekly1); + $(weekly1).append("每周 允许的通配符[, - * / L #]"); + $(weekly1).appendTo(weeklyTab); + + var weekly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "week"}).appendTo(weekly2); + $(weekly2).append("不指定"); + $(weekly2).appendTo(weeklyTab); + + var weekly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "week"}).appendTo(weekly3); + $(weekly3).append("周期 从星期"); + $("<input/>",{type : "text", id : "weekStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); + $(weekly3).append("-"); + $("<input/>",{type : "text", id : "weekEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); + $(weekly3).appendTo(weeklyTab); + + var weekly4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "week"}).appendTo(weekly4); + $(weekly4).append("第"); + $("<input/>",{type : "text", id : "weekStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); + $(weekly4).append("周的星期"); + $("<input/>",{type : "text", id : "weekEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); + $(weekly4).appendTo(weeklyTab); + + var weekly5 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "5", name : "week"}).appendTo(weekly5); + $(weekly5).append("本月最后一个星期"); + $("<input/>",{type : "text", id : "weekStart_2", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly5); + $(weekly5).appendTo(weeklyTab); + + var weekly6 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "6", name : "week", id: "week_appoint"}).appendTo(weekly6); + $(weekly6).append("指定"); + $(weekly6).appendTo(weeklyTab); + + $(weeklyTab).append('<div class="imp weekList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">1<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">2<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">3<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">4<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">5<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">6<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">7</div>'); + + $("<input/>",{type : "hidden", id : "weekHidden"}).appendTo(weeklyTab); + $(weeklyTab).appendTo(tabContent); + + //creating the yearlyTab + var yearlyTab = $("<div/>", { "class": "tab-pane", id: "Yearly" }); + + var yearly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "year"}).appendTo(yearly1); + $(yearly1).append("不指定 允许的通配符[, - * /] 非必填"); + $(yearly1).appendTo(yearlyTab); + + var yearly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "year"}).appendTo(yearly3); + $(yearly3).append("每年"); + $(yearly3).appendTo(yearlyTab); + + var yearly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "year"}).appendTo(yearly2); + $(yearly2).append("周期从"); + $("<input/>",{type : "text", id : "yearStart_0", value : "2016", style:"width:45px; height:20px;"}).appendTo(yearly2); + $(yearly2).append("-"); + $("<input/>",{type : "text", id : "yearEnd_0", value : "2017", style:"width:45px; height:20px;"}).appendTo(yearly2); + $(yearly2).append("年"); + $(yearly2).appendTo(yearlyTab); + $("<input/>",{type : "hidden", id : "yearHidden"}).appendTo(yearlyTab); + $(yearlyTab).appendTo(tabContent); + + $(tabContent).appendTo(span12); + + //creating the button and results input + // resultsName = $(this).prop("id"); + // $(this).prop("name", resultsName); + + var runTime = '<br style="padding-top: 10px"><label>最近运行时间: </label></br><textarea id="runTime" rows="6" style="width: 90%;resize: none;background: none;border: none;outline: none;" readonly = readonly></textarea></div>'; + + $(span12).appendTo(row); + $(row).appendTo(container); + $(container).appendTo(mainDiv); + $(runTime).appendTo(mainDiv); + $(cronContainer).append(mainDiv); + + var that = $(this); + + // Hide the original input + that.hide(); + + // Replace the input with an input group + var $g = $("<div>").addClass("input-group"); + // Add an input + var $i = $("<input>", { type: 'text', placeholder: 'cron表达式...', name: 'cronGen_display' }).addClass("form-control").val($(that).val()); + $i.appendTo($g); + // Add the button + var $b = $("<button class=\"btn btn-default\"><i class=\"fa fa-edit\"></i></button>"); + // Put button inside span + var $s = $("<span>").addClass("input-group-btn"); + $b.appendTo($s); + $s.appendTo($g); + + $(this).before($g); + + inputElement = that; + displayElement = $i; + + $b.popover({ + html: true, + content: function () { + return $(cronContainer).html(); + }, + template: '<div class="popover" style="max-width:500px !important; width:425px;left:-341.656px;"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>', + sanitize:false, + placement: options.direction + + }).on('click', function (e) { + if (inputElement.val().trim() !== '') { + refreshRunTime(); + } + e.preventDefault(); + + //fillDataOfMinutesAndHoursSelectOptions(); + //fillDayWeekInMonth(); + //fillInWeekDays(); + //fillInMonths(); + + $.fn.cronGen.tools.cronParse(inputElement.val()); + + //绑定指定事件 + $.fn.cronGen.tools.initChangeEvent(); + + + $('#CronGenTabs a').click(function (e) { + e.preventDefault(); + $(this).tab('show'); + //generate(); + }); + $("#CronGenMainDiv select,input").change(function (e) { + generate(); + refreshRunTime(); + }); + $("#CronGenMainDiv input").focus(function (e) { + generate(); + }); + //generate(); + }); + return; + } + }); + + + var fillInMonths = function () { + var days = [ + { text: "一月", val: "1" }, + { text: "二月", val: "2" }, + { text: "三月", val: "3" }, + { text: "四月", val: "4" }, + { text: "五月", val: "5" }, + { text: "六月", val: "6" }, + { text: "七月", val: "7" }, + { text: "八月", val: "8" }, + { text: "九月", val: "9" }, + { text: "十月", val: "10" }, + { text: "十一月", val: "11" }, + { text: "十二月", val: "12" } + ]; + $(".months").each(function () { + fillOptions(this, days); + }); + }; + + var fillOptions = function (elements, options) { + for (var i = 0; i < options.length; i++) + $(elements).append("<option value='" + options[i].val + "'>" + options[i].text + "</option>"); + }; + var fillDataOfMinutesAndHoursSelectOptions = function () { + for (var i = 0; i < 60; i++) { + if (i < 24) { + $(".hours").each(function () { $(this).append(timeSelectOption(i)); }); + } + $(".minutes").each(function () { $(this).append(timeSelectOption(i)); }); + } + }; + var fillInWeekDays = function () { + var days = [ + { text: "周一", val: "2" }, + { text: "周二", val: "3" }, + { text: "周三", val: "4" }, + { text: "周四", val: "5" }, + { text: "周五", val: "6" }, + { text: "周六", val: "7" }, + { text: "周天", val: "1" } + ]; + $(".week-days").each(function () { + fillOptions(this, days); + }); + + }; + var fillDayWeekInMonth = function () { + var days = [ + { text: "第一个", val: "1" }, + { text: "第二个", val: "2" }, + { text: "第三个", val: "3" }, + { text: "第四个", val: "4" } + ]; + $(".day-order-in-month").each(function () { + fillOptions(this, days); + }); + }; + var displayTimeUnit = function (unit) { + if (unit.toString().length == 1) + return "0" + unit; + return unit; + }; + var timeSelectOption = function (i) { + return "<option id='" + i + "'>" + displayTimeUnit(i) + "</option>"; + }; + + var generate = function () { + + var activeTab = $("ul#CronGenTabs li.active a").prop("id"); + if (activeTab == undefined) { + return; + } + var results = ""; + switch (activeTab) { + case "SecondlyTab": + switch ($("input:radio[name=second]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.cycle("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.startOn("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.initCheckBox("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "MinutesTab": + switch ($("input:radio[name=min]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.cycle("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.startOn("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.initCheckBox("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "HourlyTab": + switch ($("input:radio[name=hour]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.cycle("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.startOn("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.initCheckBox("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "DailyTab": + switch ($("input:radio[name=day]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.unAppoint("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.startOn("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "5": + $.fn.cronGen.tools.workDay("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "6": + $.fn.cronGen.tools.lastDay("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "7": + $.fn.cronGen.tools.initCheckBox("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "WeeklyTab": + switch ($("input:radio[name=week]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.unAppoint("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.startOn("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "5": + $.fn.cronGen.tools.lastWeek("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "6": + $.fn.cronGen.tools.initCheckBox("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "MonthlyTab": + switch ($("input:radio[name=month]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.unAppoint("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.startOn("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "5": + $.fn.cronGen.tools.initCheckBox("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "YearlyTab": + switch ($("input:radio[name=year]:checked").val()) { + case "1": + $.fn.cronGen.tools.unAppoint("year"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.everyTime("year"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("year"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + } + + // Update original control + inputElement.val(results); + // Update display + displayElement.val(results); + }; + + var refreshRunTime = function () { + $.ajax({ + type : 'GET', + url : base_url + "/jobinfo/nextTriggerTime", + data : { + "scheduleType" : 'CRON', + "scheduleConf" : inputElement.val() + }, + dataType : "json", + success : function(data){ + if (data.code === 200) { + $('#runTime').val(data.content.join("\n")); + } else { + $('#runTime').val(data.msg); + } + } + }); + }; + +})(jQuery); + +(function($) { + $.fn.cronGen.defaultOptions = { + direction : 'bottom' + }; + $.fn.cronGen.tools = { + /** + * 每周期 + */ + everyTime : function(dom){ + $("#"+dom+"Hidden").val("*"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 不指定 + */ + unAppoint : function(dom){ + var val = "?"; + if (dom == "year") + { + val = ""; + } + $("#"+dom+"Hidden").val(val); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 周期 + */ + cycle : function(dom){ + var start = $("#"+dom+"Start_0").val(); + var end = $("#"+dom+"End_0").val(); + $("#"+dom+"Hidden").val(start + "-" + end); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 从开始 + */ + startOn : function(dom) { + var start = $("#"+dom+"Start_1").val(); + var end = $("#"+dom+"End_1").val(); + $("#"+dom+"Hidden").val(start + "/" + end); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 最后一天 + */ + lastDay : function(dom){ + $("#"+dom+"Hidden").val("L"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 每周的某一天 + */ + weekOfDay : function(dom){ + var start = $("#"+dom+"Start_0").val(); + var end = $("#"+dom+"End_0").val(); + $("#"+dom+"Hidden").val(start + "#" + end); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 最后一周 + */ + lastWeek : function(dom){ + var start = $("#"+dom+"Start_2").val(); + $("#"+dom+"Hidden").val(start+"L"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 工作日 + */ + workDay : function(dom) { + var start = $("#"+dom+"Start_2").val(); + $("#"+dom+"Hidden").val(start + "W"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + initChangeEvent : function(){ + var secondList = $(".secondList").children(); + $("#sencond_appoint").click(function(){ + if (this.checked) { + if ($(secondList).filter(":checked").length == 0) { + $(secondList.eq(0)).attr("checked", true); + } + secondList.eq(0).change(); + } + }); + + secondList.change(function() { + var sencond_appoint = $("#sencond_appoint").prop("checked"); + if (sencond_appoint) { + var vals = []; + secondList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 59) { + val = vals.join(","); + }else if(vals.length == 59){ + val = "*"; + } + $("#secondHidden").val(val); + } + }); + + var minList = $(".minList").children(); + $("#min_appoint").click(function(){ + if (this.checked) { + if ($(minList).filter(":checked").length == 0) { + $(minList.eq(0)).attr("checked", true); + } + minList.eq(0).change(); + } + }); + + minList.change(function() { + var min_appoint = $("#min_appoint").prop("checked"); + if (min_appoint) { + var vals = []; + minList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 59) { + val = vals.join(","); + }else if(vals.length == 59){ + val = "*"; + } + $("#minHidden").val(val); + } + }); + + var hourList = $(".hourList").children(); + $("#hour_appoint").click(function(){ + if (this.checked) { + if ($(hourList).filter(":checked").length == 0) { + $(hourList.eq(0)).attr("checked", true); + } + hourList.eq(0).change(); + } + }); + + hourList.change(function() { + var hour_appoint = $("#hour_appoint").prop("checked"); + if (hour_appoint) { + var vals = []; + hourList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 24) { + val = vals.join(","); + }else if(vals.length == 24){ + val = "*"; + } + $("#hourHidden").val(val); + } + }); + + var dayList = $(".dayList").children(); + $("#day_appoint").click(function(){ + if (this.checked) { + if ($(dayList).filter(":checked").length == 0) { + $(dayList.eq(0)).attr("checked", true); + } + dayList.eq(0).change(); + } + }); + + dayList.change(function() { + var day_appoint = $("#day_appoint").prop("checked"); + if (day_appoint) { + var vals = []; + dayList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 31) { + val = vals.join(","); + }else if(vals.length == 31){ + val = "*"; + } + $("#dayHidden").val(val); + } + }); + + var monthList = $(".monthList").children(); + $("#month_appoint").click(function(){ + if (this.checked) { + if ($(monthList).filter(":checked").length == 0) { + $(monthList.eq(0)).attr("checked", true); + } + monthList.eq(0).change(); + } + }); + + monthList.change(function() { + var month_appoint = $("#month_appoint").prop("checked"); + if (month_appoint) { + var vals = []; + monthList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 12) { + val = vals.join(","); + }else if(vals.length == 12){ + val = "*"; + } + $("#monthHidden").val(val); + } + }); + + var weekList = $(".weekList").children(); + $("#week_appoint").click(function(){ + if (this.checked) { + if ($(weekList).filter(":checked").length == 0) { + $(weekList.eq(0)).attr("checked", true); + } + weekList.eq(0).change(); + } + }); + + weekList.change(function() { + var week_appoint = $("#week_appoint").prop("checked"); + if (week_appoint) { + var vals = []; + weekList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 7) { + val = vals.join(","); + }else if(vals.length == 7){ + val = "*"; + } + $("#weekHidden").val(val); + } + }); + }, + initObj : function(strVal, strid){ + var ary = null; + var objRadio = $("input[name='" + strid + "'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(1).attr("checked", "checked"); + $("#" + strid + "Start_0").val(ary[0]); + $("#" + strid + "End_0").val(ary[1]); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(2).attr("checked", "checked"); + $("#" + strid + "Start_1").val(ary[0]); + $("#" + strid + "End_1").val(ary[1]); + } else { + objRadio.eq(3).attr("checked", "checked"); + if (strVal != "?") { + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $("." + strid + "List input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox(strid); + } + } + }, + initDay : function(strVal) { + var ary = null; + var objRadio = $("input[name='day'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal == "?") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(2).attr("checked", "checked"); + $("#dayStart_0").val(ary[0]); + $("#dayEnd_0").val(ary[1]); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(3).attr("checked", "checked"); + $("#dayStart_1").val(ary[0]); + $("#dayEnd_1").val(ary[1]); + } else if (strVal.split('W').length > 1) { + ary = strVal.split('W'); + objRadio.eq(4).attr("checked", "checked"); + $("#dayStart_2").val(ary[0]); + } else if (strVal == "L") { + objRadio.eq(5).attr("checked", "checked"); + } else { + objRadio.eq(6).attr("checked", "checked"); + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $(".dayList input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox("day"); + } + }, + initMonth : function(strVal) { + var ary = null; + var objRadio = $("input[name='month'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal == "?") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(2).attr("checked", "checked"); + $("#monthStart_0").val(ary[0]); + $("#monthEnd_0").val(ary[1]); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(3).attr("checked", "checked"); + $("#monthStart_1").val(ary[0]); + $("#monthEnd_1").val(ary[1]); + + } else { + objRadio.eq(4).attr("checked", "checked"); + + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $(".monthList input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox("month"); + } + }, + initWeek : function(strVal) { + var ary = null; + var objRadio = $("input[name='week'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal == "?") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(2).attr("checked", "checked"); + $("#weekStart_0").val(ary[0]); + $("#weekEnd_0").val(ary[1]); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(3).attr("checked", "checked"); + $("#weekStart_1").val(ary[0]); + $("#weekEnd_1").val(ary[1]); + } else if (strVal.split('L').length > 1) { + ary = strVal.split('L'); + objRadio.eq(4).attr("checked", "checked"); + $("#weekStart_2").val(ary[0]); + } else { + objRadio.eq(5).attr("checked", "checked"); + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $(".weekList input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox("week"); + } + }, + initYear : function(strVal) { + var ary = null; + var objRadio = $("input[name='year'"); + if (strVal == "*") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(2).attr("checked", "checked"); + $("#yearStart_0").val(ary[0]); + $("#yearEnd_0").val(ary[1]); + } + }, + cronParse : function(cronExpress) { + //获取参数中表达式的值 + if (cronExpress) { + var regs = cronExpress.split(' '); + $("#secondHidden").val(regs[0]); + $("#minHidden").val(regs[1]); + $("#hourHidden").val(regs[2]); + $("#dayHidden").val(regs[3]); + $("#monthHidden").val(regs[4]); + $("#weekHidden").val(regs[5]); + + $.fn.cronGen.tools.initObj(regs[0], "second"); + $.fn.cronGen.tools.initObj(regs[1], "min"); + $.fn.cronGen.tools.initObj(regs[2], "hour"); + $.fn.cronGen.tools.initDay(regs[3]); + $.fn.cronGen.tools.initMonth(regs[4]); + $.fn.cronGen.tools.initWeek(regs[5]); + + if (regs.length > 6) { + $("input[name=yearHidden]").val(regs[6]); + $.fn.cronGen.tools.initYear(regs[6]); + } + } + }, + cronResult : function() { + var result; + var second = $("#secondHidden").val(); + second = second== "" ? "*":second; + var minute = $("#minHidden").val(); + minute = minute== "" ? "*":minute; + var hour = $("#hourHidden").val(); + hour = hour== "" ? "*":hour; + var day = $("#dayHidden").val(); + day = day== "" ? "*":day; + var month = $("#monthHidden").val(); + month = month== "" ? "*":month; + var week = $("#weekHidden").val(); + week = week== "" ? "?":week; + var year = $("#yearHidden").val(); + if(year!="") + { + result = second+" "+minute+" "+hour+" "+day+" "+month+" "+week+" "+year; + }else + { + result = second+" "+minute+" "+hour+" "+day+" "+month+" "+week; + } + return result; + }, + clearCheckbox : function(dom){ + //清除选中的checkbox + var list = $("."+dom+"List").children().filter(":checked"); + if ($(list).length > 0) { + $.each(list, function(index){ + $(this).attr("checked", false); + $(this).attr("disabled", "disabled"); + $(this).change(); + }); + } + }, + initCheckBox : function(dom) { + //移除checkbox禁用 + var list = $("."+dom+"List").children(); + if ($(list).length > 0) { + $.each(list, function(index){ + $(this).removeAttr("disabled"); + }); + } + } + }; +})(jQuery); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js new file mode 100644 index 0000000..cbf84ee --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js @@ -0,0 +1,1106 @@ +(function ($) { + // var resultsName = ""; + var inputElement; + var displayElement; + $.fn.extend({ + cronGen: function (options) { + if (options == null) { + options = {}; + } + options = $.extend({}, $.fn.cronGen.defaultOptions, options); + //create top menu + var cronContainer = $("<div/>", { id: "CronContainer", style: "display:none;width:300px;height:300px;" }); + var mainDiv = $("<div/>", { id: "CronGenMainDiv", style: "width:410px;height:420px;" }); + var topMenu = $("<ul/>", { "class": "nav nav-tabs", id: "CronGenTabs" }); + $('<li/>', { 'class': 'active' }).html($('<a id="SecondlyTab" href="#Secondly">秒</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="MinutesTab" href="#Minutes">Minute</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="HourlyTab" href="#Hourly">Hour</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="DailyTab" href="#Daily">Day</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="MonthlyTab" href="#Monthly">Month</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="WeeklyTab" href="#Weekly">Week</a>')).appendTo(topMenu); + $('<li/>').html($('<a id="YearlyTab" href="#Yearly">Year</a>')).appendTo(topMenu); + $(topMenu).appendTo(mainDiv); + + //create what's inside the tabs + var container = $("<div/>", { "class": "container-fluid", "style": "margin-top: 30px;margin-left: -14px;" }); + var row = $("<div/>", { "class": "row-fluid" }); + var span12 = $("<div/>", { "class": "span12" }); + var tabContent = $("<div/>", { "class": "tab-content", "style": "border:0px; margin-top:-20px;" }); + + + //creating the secondsTab + var secondsTab = $("<div/>", { "class": "tab-pane active", id: "Secondly" }); + var seconds1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "second"}).appendTo(seconds1); + $(seconds1).append("Per second, allowed wildcard[, - * /]"); + $(seconds1).appendTo(secondsTab); + + var seconds2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "second"}).appendTo(seconds2); + $(seconds2).append("Cycle, from"); + $("<input/>",{type : "text", id : "secondStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds2); + $(seconds2).append("-"); + $("<input/>",{type : "text", id : "secondEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds2); + $(seconds2).append("second"); + $(seconds2).appendTo(secondsTab); + + var seconds3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "second"}).appendTo(seconds3); + $(seconds3).append("from"); + $("<input/>",{type : "text", id : "secondStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds3); + $(seconds3).append("seconds start, per"); + $("<input/>",{type : "text", id : "secondEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds3); + $(seconds3).append("second execute once"); + $(seconds3).appendTo(secondsTab); + + var seconds4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "second", id: "sencond_appoint"}).appendTo(seconds4); + $(seconds4).append("specify"); + $(seconds4).appendTo(secondsTab); + + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="0">00<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="24">24<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="25">25<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="26">26<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="27">27<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="28">28<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="29">29</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="30">30<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="31">31<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="32">32<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="33">33<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="34">34<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="35">35<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="36">36<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="37">37<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="38">38<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="39">39</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="40">40<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="41">41<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="42">42<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="43">43<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="44">44<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="45">45<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="46">46<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="47">47<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="48">48<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="49">49</div>'); + $(secondsTab).append('<div class="imp secondList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="50">50<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="51">51<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="52">52<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="53">53<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="54">54<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="55">55<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="56">56<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="57">57<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="58">58<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="59">59</div>'); + $("<input/>",{type : "hidden", id : "secondHidden"}).appendTo(secondsTab); + $(secondsTab).appendTo(tabContent); + + //creating the minutesTab + var minutesTab = $("<div/>", { "class": "tab-pane", id: "Minutes" }); + + var minutes1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "min"}).appendTo(minutes1); + $(minutes1).append("Per minute, allowed wildcard[, - * /]"); + $(minutes1).appendTo(minutesTab); + + var minutes2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "min"}).appendTo(minutes2); + $(minutes2).append("Cycle, from"); + $("<input/>",{type : "text", id : "minStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes2); + $(minutes2).append("-"); + $("<input/>",{type : "text", id : "minEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes2); + $(minutes2).append("minute"); + $(minutes2).appendTo(minutesTab); + + var minutes3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "min"}).appendTo(minutes3); + $(minutes3).append("from"); + $("<input/>",{type : "text", id : "minStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes3); + $(minutes3).append("seconds start, per"); + $("<input/>",{type : "text", id : "minEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes3); + $(minutes3).append("second execute once"); + $(minutes3).appendTo(minutesTab); + + var minutes4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "min", id: "min_appoint"}).appendTo(minutes4); + $(minutes4).append("specify"); + $(minutes4).appendTo(minutesTab); + + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="0">00<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="24">24<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="25">25<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="26">26<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="27">27<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="28">28<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="29">29</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="30">30<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="31">31<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="32">32<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="33">33<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="34">34<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="35">35<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="36">36<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="37">37<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="38">38<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="39">39</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="40">40<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="41">41<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="42">42<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="43">43<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="44">44<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="45">45<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="46">46<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="47">47<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="48">48<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="49">49</div>'); + $(minutesTab).append('<div class="imp minList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="50">50<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="51">51<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="52">52<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="53">53<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="54">54<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="55">55<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="56">56<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="57">57<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="58">58<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="59">59</div>'); + $("<input/>",{type : "hidden", id : "minHidden"}).appendTo(minutesTab); + $(minutesTab).appendTo(tabContent); + + //creating the hourlyTab + var hourlyTab = $("<div/>", { "class": "tab-pane", id: "Hourly" }); + + var hourly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "hour"}).appendTo(hourly1); + $(hourly1).append("Per hour, allowed wildcard[, - * /]"); + $(hourly1).appendTo(hourlyTab); + + var hourly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "hour"}).appendTo(hourly2); + $(hourly2).append("Cycle, from"); + $("<input/>",{type : "text", id : "hourStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly2); + $(hourly2).append("-"); + $("<input/>",{type : "text", id : "hourEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly2); + $(hourly2).append("hour"); + $(hourly2).appendTo(hourlyTab); + + var hourly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "hour"}).appendTo(hourly3); + $(hourly3).append("from"); + $("<input/>",{type : "text", id : "hourStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly3); + $(hourly3).append("hour start, per"); + $("<input/>",{type : "text", id : "hourEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly3); + $(hourly3).append("hour execute once"); + $(hourly3).appendTo(hourlyTab); + + var hourly4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "hour", id: "hour_appoint"}).appendTo(hourly4); + $(hourly4).append("specify"); + $(hourly4).appendTo(hourlyTab); + + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="0">00<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05</div>'); + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11</div>'); + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17</div>'); + $(hourlyTab).append('<div class="imp hourList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23</div>'); + $("<input/>",{type : "hidden", id : "hourHidden"}).appendTo(hourlyTab); + $(hourlyTab).appendTo(tabContent); + + + //creating the dailyTab + var dailyTab = $("<div/>", { "class": "tab-pane", id: "Daily" }); + + var daily1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "day"}).appendTo(daily1); + $(daily1).append("Per day, allowed wildcard[, - * / L W]"); + $(daily1).appendTo(dailyTab); + + var daily5 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "day"}).appendTo(daily5); + $(daily5).append("not specify"); + $(daily5).appendTo(dailyTab); + + var daily2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "day"}).appendTo(daily2); + $(daily2).append("Cycle, from"); + $("<input/>",{type : "text", id : "dayStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily2); + $(daily2).append("-"); + $("<input/>",{type : "text", id : "dayEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily2); + $(daily2).append("day"); + $(daily2).appendTo(dailyTab); + + var daily3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "day"}).appendTo(daily3); + $(daily3).append("from"); + $("<input/>",{type : "text", id : "dayStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily3); + $(daily3).append("day start, per"); + $("<input/>",{type : "text", id : "dayEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily3); + $(daily3).append("day execute once"); + $(daily3).appendTo(dailyTab); + + var daily6 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "5", name : "day"}).appendTo(daily6); + $(daily6).append("The most recent working day on the 1"); + $("<input/>",{type : "text", id : "dayStart_2", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily6); + $(daily6).append(" of each month"); + $(daily6).appendTo(dailyTab); + + var daily7 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "6", name : "day"}).appendTo(daily7); + $(daily7).append("The last day of the month"); + $(daily7).appendTo(dailyTab); + + var daily4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "7", name : "day", id: "day_appoint"}).appendTo(daily4); + $(daily4).append("specify"); + $(daily4).appendTo(dailyTab); + + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10</div>'); + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="13">13<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="14">14<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="15">15<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="16">16<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="17">17<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="18">18<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="19">19<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="20">20</div>'); + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="21">21<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="22">22<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="23">23<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="24">24<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="25">25<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="26">26<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="27">27<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="28">28<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="29">29<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="30">30</div>'); + $(dailyTab).append('<div class="imp dayList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="31">31</div>'); + $("<input/>",{type : "hidden", id : "dayHidden"}).appendTo(dailyTab); + $(dailyTab).appendTo(tabContent); + + + //creating the monthlyTab + var monthlyTab = $("<div/>", { "class": "tab-pane", id: "Monthly" }); + + var monthly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "month"}).appendTo(monthly1); + $(monthly1).append("Per month, allowed wildcard[, - * /]"); + $(monthly1).appendTo(monthlyTab); + + var monthly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "month"}).appendTo(monthly2); + $(monthly2).append("not specify"); + $(monthly2).appendTo(monthlyTab); + + var monthly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "month"}).appendTo(monthly3); + $(monthly3).append("Cycle, from"); + $("<input/>",{type : "text", id : "monthStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly3); + $(monthly3).append("-"); + $("<input/>",{type : "text", id : "monthEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly3); + $(monthly3).append("month"); + $(monthly3).appendTo(monthlyTab); + + var monthly4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "month"}).appendTo(monthly4); + $(monthly4).append("Starting from "); + $("<input/>",{type : "text", id : "monthStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly4); + $(monthly4).append("day, once every"); + $("<input/>",{type : "text", id : "monthEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly4); + $(monthly4).append("month"); + $(monthly4).appendTo(monthlyTab); + + var monthly5 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "5", name : "month", id: "month_appoint"}).appendTo(monthly5); + $(monthly5).append("specify"); + $(monthly5).appendTo(monthlyTab); + + $(monthlyTab).append('<div class="imp monthList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">01<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">02<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">03<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">04<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">05<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">06</div>'); + $(monthlyTab).append('<div class="imp monthList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">07<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="8">08<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="9">09<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="10">10<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="11">11<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="12">12</div>'); + $("<input/>",{type : "hidden", id : "monthHidden"}).appendTo(monthlyTab); + $(monthlyTab).appendTo(tabContent); + + //creating the weeklyTab + var weeklyTab = $("<div/>", { "class": "tab-pane", id: "Weekly" }); + + var weekly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "week"}).appendTo(weekly1); + $(weekly1).append("Per week, allowed wildcard[, - * / L #]"); + $(weekly1).appendTo(weeklyTab); + + var weekly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "week"}).appendTo(weekly2); + $(weekly2).append("not specify"); + $(weekly2).appendTo(weeklyTab); + + var weekly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "week"}).appendTo(weekly3); + $(weekly3).append("Cycle, from week"); + $("<input/>",{type : "text", id : "weekStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); + $(weekly3).append("-"); + $("<input/>",{type : "text", id : "weekEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); + $(weekly3).appendTo(weeklyTab); + + var weekly4 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "4", name : "week"}).appendTo(weekly4); + $(weekly4).append("The"); + $("<input/>",{type : "text", id : "weekStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); + $(weekly4).append("th week, and day "); + $("<input/>",{type : "text", id : "weekEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); + $(weekly4).appendTo(weeklyTab); + + var weekly5 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "5", name : "week"}).appendTo(weekly5); + $(weekly5).append("Last week of the month"); + $("<input/>",{type : "text", id : "weekStart_2", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly5); + $(weekly5).appendTo(weeklyTab); + + var weekly6 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "6", name : "week", id: "week_appoint"}).appendTo(weekly6); + $(weekly6).append("specify"); + $(weekly6).appendTo(weeklyTab); + + $(weeklyTab).append('<div class="imp weekList"><input type="checkbox" disabled="disabled" style="margin-left: 5px" value="1">1<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="2">2<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="3">3<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="4">4<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="5">5<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="6">6<input type="checkbox" disabled="disabled" style="margin-left: 5px" value="7">7</div>'); + + $("<input/>",{type : "hidden", id : "weekHidden"}).appendTo(weeklyTab); + $(weeklyTab).appendTo(tabContent); + + //creating the yearlyTab + var yearlyTab = $("<div/>", { "class": "tab-pane", id: "Yearly" }); + + var yearly1 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "1", name : "year"}).appendTo(yearly1); + $(yearly1).append("not specify allowed wildcard[, - * /] not required"); + $(yearly1).appendTo(yearlyTab); + + var yearly3 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "2", name : "year"}).appendTo(yearly3); + $(yearly3).append("Per year"); + $(yearly3).appendTo(yearlyTab); + + var yearly2 = $("<div/>",{"class":"line"}); + $("<input/>",{type : "radio", value : "3", name : "year"}).appendTo(yearly2); + $(yearly2).append("Cycle, from "); + $("<input/>",{type : "text", id : "yearStart_0", value : "2016", style:"width:45px; height:20px;"}).appendTo(yearly2); + $(yearly2).append("-"); + $("<input/>",{type : "text", id : "yearEnd_0", value : "2017", style:"width:45px; height:20px;"}).appendTo(yearly2); + $(yearly2).append("year"); + $(yearly2).appendTo(yearlyTab); + $("<input/>",{type : "hidden", id : "yearHidden"}).appendTo(yearlyTab); + $(yearlyTab).appendTo(tabContent); + + $(tabContent).appendTo(span12); + + //creating the button and results input + // resultsName = $(this).prop("id"); + // $(this).prop("name", resultsName); + + var runTime = '<br style="padding-top: 10px"><label>Recent Run Time: </label></br><textarea id="runTime" rows="6" style="width: 90%;resize: none;background: none;border: none;outline: none;" readonly = readonly></textarea></div>'; + + $(span12).appendTo(row); + $(row).appendTo(container); + $(container).appendTo(mainDiv); + $(runTime).appendTo(mainDiv); + $(cronContainer).append(mainDiv); + + var that = $(this); + + // Hide the original input + that.hide(); + + // Replace the input with an input group + var $g = $("<div>").addClass("input-group"); + // Add an input + var $i = $("<input>", { type: 'text', placeholder: 'cron expression...', name: 'cronGen_display' }).addClass("form-control").val($(that).val()); + $i.appendTo($g); + // Add the button + var $b = $("<button class=\"btn btn-default\"><i class=\"fa fa-edit\"></i></button>"); + // Put button inside span + var $s = $("<span>").addClass("input-group-btn"); + $b.appendTo($s); + $s.appendTo($g); + + $(this).before($g); + + inputElement = that; + displayElement = $i; + + $b.popover({ + html: true, + content: function () { + return $(cronContainer).html(); + }, + template: '<div class="popover" style="max-width:500px !important; width:425px;left:-341.656px;"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>', + sanitize:false, + placement: options.direction + + }).on('click', function (e) { + if (inputElement.val().trim() !== '') { + refreshRunTime(); + } + e.preventDefault(); + + //fillDataOfMinutesAndHoursSelectOptions(); + //fillDayWeekInMonth(); + //fillInWeekDays(); + //fillInMonths(); + + $.fn.cronGen.tools.cronParse(inputElement.val()); + + //绑定指定事件 + $.fn.cronGen.tools.initChangeEvent(); + + + $('#CronGenTabs a').click(function (e) { + e.preventDefault(); + $(this).tab('show'); + //generate(); + }); + $("#CronGenMainDiv select,input").change(function (e) { + generate(); + refreshRunTime(); + }); + $("#CronGenMainDiv input").focus(function (e) { + generate(); + }); + //generate(); + }); + return; + } + }); + + + var fillInMonths = function () { + var days = [ + { text: "January", val: "1" }, + { text: "February", val: "2" }, + { text: "March", val: "3" }, + { text: "April", val: "4" }, + { text: "May", val: "5" }, + { text: "June", val: "6" }, + { text: "July", val: "7" }, + { text: "August", val: "8" }, + { text: "September", val: "9" }, + { text: "October", val: "10" }, + { text: "November", val: "11" }, + { text: "December", val: "12" } + ]; + $(".months").each(function () { + fillOptions(this, days); + }); + }; + + var fillOptions = function (elements, options) { + for (var i = 0; i < options.length; i++) + $(elements).append("<option value='" + options[i].val + "'>" + options[i].text + "</option>"); + }; + var fillDataOfMinutesAndHoursSelectOptions = function () { + for (var i = 0; i < 60; i++) { + if (i < 24) { + $(".hours").each(function () { $(this).append(timeSelectOption(i)); }); + } + $(".minutes").each(function () { $(this).append(timeSelectOption(i)); }); + } + }; + var fillInWeekDays = function () { + var days = [ + { text: "Tuesday", val: "2" }, + { text: "Wednesday", val: "3" }, + { text: "Thursday", val: "4" }, + { text: "Friday", val: "5" }, + { text: "Saturday", val: "6" }, + { text: "Sunday", val: "7" }, + { text: "Monday", val: "1" } + ]; + $(".week-days").each(function () { + fillOptions(this, days); + }); + + }; + var fillDayWeekInMonth = function () { + var days = [ + { text: "First", val: "1" }, + { text: "Second", val: "2" }, + { text: "Third", val: "3" }, + { text: "Fourth", val: "4" } + ]; + $(".day-order-in-month").each(function () { + fillOptions(this, days); + }); + }; + var displayTimeUnit = function (unit) { + if (unit.toString().length == 1) + return "0" + unit; + return unit; + }; + var timeSelectOption = function (i) { + return "<option id='" + i + "'>" + displayTimeUnit(i) + "</option>"; + }; + + var generate = function () { + + var activeTab = $("ul#CronGenTabs li.active a").prop("id"); + if (activeTab == undefined) { + return; + } + var results = ""; + switch (activeTab) { + case "SecondlyTab": + switch ($("input:radio[name=second]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.cycle("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.startOn("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.initCheckBox("second"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "MinutesTab": + switch ($("input:radio[name=min]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.cycle("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.startOn("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.initCheckBox("min"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "HourlyTab": + switch ($("input:radio[name=hour]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.cycle("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.startOn("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.initCheckBox("hour"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "DailyTab": + switch ($("input:radio[name=day]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.unAppoint("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.startOn("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "5": + $.fn.cronGen.tools.workDay("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "6": + $.fn.cronGen.tools.lastDay("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "7": + $.fn.cronGen.tools.initCheckBox("day"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "WeeklyTab": + switch ($("input:radio[name=week]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.unAppoint("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.startOn("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "5": + $.fn.cronGen.tools.lastWeek("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "6": + $.fn.cronGen.tools.initCheckBox("week"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "MonthlyTab": + switch ($("input:radio[name=month]:checked").val()) { + case "1": + $.fn.cronGen.tools.everyTime("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.unAppoint("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "4": + $.fn.cronGen.tools.startOn("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "5": + $.fn.cronGen.tools.initCheckBox("month"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + case "YearlyTab": + switch ($("input:radio[name=year]:checked").val()) { + case "1": + $.fn.cronGen.tools.unAppoint("year"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "2": + $.fn.cronGen.tools.everyTime("year"); + results = $.fn.cronGen.tools.cronResult(); + break; + case "3": + $.fn.cronGen.tools.cycle("year"); + results = $.fn.cronGen.tools.cronResult(); + break; + } + break; + } + + // Update original control + inputElement.val(results); + // Update display + displayElement.val(results); + }; + + var refreshRunTime = function () { + $.ajax({ + type : 'GET', + url : base_url + "/jobinfo/nextTriggerTime", + data : { + "scheduleType" : 'CRON', + "scheduleConf" : inputElement.val() + }, + dataType : "json", + success : function(data){ + if (data.code === 200) { + $('#runTime').val(data.content.join("\n")); + } else { + $('#runTime').val(data.msg); + } + } + }); + }; + +})(jQuery); + +(function($) { + $.fn.cronGen.defaultOptions = { + direction : 'bottom' + }; + $.fn.cronGen.tools = { + /** + * 每周期 + */ + everyTime : function(dom){ + $("#"+dom+"Hidden").val("*"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 不指定 + */ + unAppoint : function(dom){ + var val = "?"; + if (dom == "year") + { + val = ""; + } + $("#"+dom+"Hidden").val(val); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 周期 + */ + cycle : function(dom){ + var start = $("#"+dom+"Start_0").val(); + var end = $("#"+dom+"End_0").val(); + $("#"+dom+"Hidden").val(start + "-" + end); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 从开始 + */ + startOn : function(dom) { + var start = $("#"+dom+"Start_1").val(); + var end = $("#"+dom+"End_1").val(); + $("#"+dom+"Hidden").val(start + "/" + end); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 最后一天 + */ + lastDay : function(dom){ + $("#"+dom+"Hidden").val("L"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 每周的某一天 + */ + weekOfDay : function(dom){ + var start = $("#"+dom+"Start_0").val(); + var end = $("#"+dom+"End_0").val(); + $("#"+dom+"Hidden").val(start + "#" + end); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 最后一周 + */ + lastWeek : function(dom){ + var start = $("#"+dom+"Start_2").val(); + $("#"+dom+"Hidden").val(start+"L"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + /** + * 工作日 + */ + workDay : function(dom) { + var start = $("#"+dom+"Start_2").val(); + $("#"+dom+"Hidden").val(start + "W"); + $.fn.cronGen.tools.clearCheckbox(dom); + }, + initChangeEvent : function(){ + var secondList = $(".secondList").children(); + $("#sencond_appoint").click(function(){ + if (this.checked) { + if ($(secondList).filter(":checked").length == 0) { + $(secondList.eq(0)).attr("checked", true); + } + secondList.eq(0).change(); + } + }); + + secondList.change(function() { + var sencond_appoint = $("#sencond_appoint").prop("checked"); + if (sencond_appoint) { + var vals = []; + secondList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 59) { + val = vals.join(","); + }else if(vals.length == 59){ + val = "*"; + } + $("#secondHidden").val(val); + } + }); + + var minList = $(".minList").children(); + $("#min_appoint").click(function(){ + if (this.checked) { + if ($(minList).filter(":checked").length == 0) { + $(minList.eq(0)).attr("checked", true); + } + minList.eq(0).change(); + } + }); + + minList.change(function() { + var min_appoint = $("#min_appoint").prop("checked"); + if (min_appoint) { + var vals = []; + minList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 59) { + val = vals.join(","); + }else if(vals.length == 59){ + val = "*"; + } + $("#minHidden").val(val); + } + }); + + var hourList = $(".hourList").children(); + $("#hour_appoint").click(function(){ + if (this.checked) { + if ($(hourList).filter(":checked").length == 0) { + $(hourList.eq(0)).attr("checked", true); + } + hourList.eq(0).change(); + } + }); + + hourList.change(function() { + var hour_appoint = $("#hour_appoint").prop("checked"); + if (hour_appoint) { + var vals = []; + hourList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 24) { + val = vals.join(","); + }else if(vals.length == 24){ + val = "*"; + } + $("#hourHidden").val(val); + } + }); + + var dayList = $(".dayList").children(); + $("#day_appoint").click(function(){ + if (this.checked) { + if ($(dayList).filter(":checked").length == 0) { + $(dayList.eq(0)).attr("checked", true); + } + dayList.eq(0).change(); + } + }); + + dayList.change(function() { + var day_appoint = $("#day_appoint").prop("checked"); + if (day_appoint) { + var vals = []; + dayList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 31) { + val = vals.join(","); + }else if(vals.length == 31){ + val = "*"; + } + $("#dayHidden").val(val); + } + }); + + var monthList = $(".monthList").children(); + $("#month_appoint").click(function(){ + if (this.checked) { + if ($(monthList).filter(":checked").length == 0) { + $(monthList.eq(0)).attr("checked", true); + } + monthList.eq(0).change(); + } + }); + + monthList.change(function() { + var month_appoint = $("#month_appoint").prop("checked"); + if (month_appoint) { + var vals = []; + monthList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 12) { + val = vals.join(","); + }else if(vals.length == 12){ + val = "*"; + } + $("#monthHidden").val(val); + } + }); + + var weekList = $(".weekList").children(); + $("#week_appoint").click(function(){ + if (this.checked) { + if ($(weekList).filter(":checked").length == 0) { + $(weekList.eq(0)).attr("checked", true); + } + weekList.eq(0).change(); + } + }); + + weekList.change(function() { + var week_appoint = $("#week_appoint").prop("checked"); + if (week_appoint) { + var vals = []; + weekList.each(function() { + if (this.checked) { + vals.push(this.value); + } + }); + var val = "?"; + if (vals.length > 0 && vals.length < 7) { + val = vals.join(","); + }else if(vals.length == 7){ + val = "*"; + } + $("#weekHidden").val(val); + } + }); + }, + initObj : function(strVal, strid){ + var ary = null; + var objRadio = $("input[name='" + strid + "'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(1).attr("checked", "checked"); + $("#" + strid + "Start_0").val(ary[0]); + $("#" + strid + "End_0").val(ary[1]); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(2).attr("checked", "checked"); + $("#" + strid + "Start_1").val(ary[0]); + $("#" + strid + "End_1").val(ary[1]); + } else { + objRadio.eq(3).attr("checked", "checked"); + if (strVal != "?") { + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $("." + strid + "List input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox(strid); + } + } + }, + initDay : function(strVal) { + var ary = null; + var objRadio = $("input[name='day'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal == "?") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(2).attr("checked", "checked"); + $("#dayStart_0").val(ary[0]); + $("#dayEnd_0").val(ary[1]); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(3).attr("checked", "checked"); + $("#dayStart_1").val(ary[0]); + $("#dayEnd_1").val(ary[1]); + } else if (strVal.split('W').length > 1) { + ary = strVal.split('W'); + objRadio.eq(4).attr("checked", "checked"); + $("#dayStart_2").val(ary[0]); + } else if (strVal == "L") { + objRadio.eq(5).attr("checked", "checked"); + } else { + objRadio.eq(6).attr("checked", "checked"); + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $(".dayList input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox("day"); + } + }, + initMonth : function(strVal) { + var ary = null; + var objRadio = $("input[name='month'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal == "?") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(2).attr("checked", "checked"); + $("#monthStart_0").val(ary[0]); + $("#monthEnd_0").val(ary[1]); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(3).attr("checked", "checked"); + $("#monthStart_1").val(ary[0]); + $("#monthEnd_1").val(ary[1]); + + } else { + objRadio.eq(4).attr("checked", "checked"); + + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $(".monthList input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox("month"); + } + }, + initWeek : function(strVal) { + var ary = null; + var objRadio = $("input[name='week'"); + if (strVal == "*") { + objRadio.eq(0).attr("checked", "checked"); + } else if (strVal == "?") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('/').length > 1) { + ary = strVal.split('/'); + objRadio.eq(2).attr("checked", "checked"); + $("#weekStart_0").val(ary[0]); + $("#weekEnd_0").val(ary[1]); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(3).attr("checked", "checked"); + $("#weekStart_1").val(ary[0]); + $("#weekEnd_1").val(ary[1]); + } else if (strVal.split('L').length > 1) { + ary = strVal.split('L'); + objRadio.eq(4).attr("checked", "checked"); + $("#weekStart_2").val(ary[0]); + } else { + objRadio.eq(5).attr("checked", "checked"); + ary = strVal.split(","); + for (var i = 0; i < ary.length; i++) { + $(".weekList input[value='" + ary[i] + "']").attr("checked", "checked"); + } + $.fn.cronGen.tools.initCheckBox("week"); + } + }, + initYear : function(strVal) { + var ary = null; + var objRadio = $("input[name='year'"); + if (strVal == "*") { + objRadio.eq(1).attr("checked", "checked"); + } else if (strVal.split('-').length > 1) { + ary = strVal.split('-'); + objRadio.eq(2).attr("checked", "checked"); + $("#yearStart_0").val(ary[0]); + $("#yearEnd_0").val(ary[1]); + } + }, + cronParse : function(cronExpress) { + //获取参数中表达式的值 + if (cronExpress) { + var regs = cronExpress.split(' '); + $("#secondHidden").val(regs[0]); + $("#minHidden").val(regs[1]); + $("#hourHidden").val(regs[2]); + $("#dayHidden").val(regs[3]); + $("#monthHidden").val(regs[4]); + $("#weekHidden").val(regs[5]); + + $.fn.cronGen.tools.initObj(regs[0], "second"); + $.fn.cronGen.tools.initObj(regs[1], "min"); + $.fn.cronGen.tools.initObj(regs[2], "hour"); + $.fn.cronGen.tools.initDay(regs[3]); + $.fn.cronGen.tools.initMonth(regs[4]); + $.fn.cronGen.tools.initWeek(regs[5]); + + if (regs.length > 6) { + $("input[name=yearHidden]").val(regs[6]); + $.fn.cronGen.tools.initYear(regs[6]); + } + } + }, + cronResult : function() { + var result; + var second = $("#secondHidden").val(); + second = second== "" ? "*":second; + var minute = $("#minHidden").val(); + minute = minute== "" ? "*":minute; + var hour = $("#hourHidden").val(); + hour = hour== "" ? "*":hour; + var day = $("#dayHidden").val(); + day = day== "" ? "*":day; + var month = $("#monthHidden").val(); + month = month== "" ? "*":month; + var week = $("#weekHidden").val(); + week = week== "" ? "?":week; + var year = $("#yearHidden").val(); + if(year!="") + { + result = second+" "+minute+" "+hour+" "+day+" "+month+" "+week+" "+year; + }else + { + result = second+" "+minute+" "+hour+" "+day+" "+month+" "+week; + } + return result; + }, + clearCheckbox : function(dom){ + //清除选中的checkbox + var list = $("."+dom+"List").children().filter(":checked"); + if ($(list).length > 0) { + $.each(list, function(index){ + $(this).attr("checked", false); + $(this).attr("disabled", "disabled"); + $(this).change(); + }); + } + }, + initCheckBox : function(dom) { + //移除checkbox禁用 + var list = $("."+dom+"List").children(); + if ($(list).length > 0) { + $.each(list, function(index){ + $(this).removeAttr("disabled"); + }); + } + } + }; +})(jQuery); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js new file mode 100644 index 0000000..cdf14db --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js @@ -0,0 +1,22 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.echarts={})}(this,function(t){"use strict";function e(t,e){"createCanvas"===t&&(zp=null),Lp[t]=e}function n(t){if(null==t||"object"!=typeof t)return t;var e=t,i=Ip.call(t);if("[object Array]"===i){if(!z(t)){e=[];for(var r=0,o=t.length;r<o;r++)e[r]=n(t[r])}}else if(Sp[i]){if(!z(t)){var a=t.constructor;if(t.constructor.from)e=a.from(t);else{e=new a(t.length);for(var r=0,o=t.length;r<o;r++)e[r]=n(t[r])}}}else if(!Mp[i]&&!z(t)&&!S(t)){e={};for(var s in t)t.hasOwnProperty(s)&&(e[s]=n(t[s]))}return e}function i(t,e,r){if(!w(e)||!w(t))return r?n(e):t;for(var o in e)if(e.hasOwnProperty(o)){var a=t[o],s=e[o];!w(s)||!w(a)||y(s)||y(a)||S(s)||S(a)||b(s)||b(a)||z(s)||z(a)?!r&&o in t||(t[o]=n(e[o],!0)):i(a,s,r)}return t}function r(t,e){for(var n=t[0],r=1,o=t.length;r<o;r++)n=i(n,t[r],e);return n}function o(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function a(t,e,n){for(var i in e)e.hasOwnProperty(i)&&(n?null!=e[i]:null==t[i])&&(t[i]=e[i]);return t}function s(){return zp||(zp=Op().getContext("2d")),zp}function l(t,e){if(t){if(t.indexOf)return t.indexOf(e);for(var n=0,i=t.length;n<i;n++)if(t[n]===e)return n}return-1}function u(t,e){function n(){}var i=t.prototype;n.prototype=e.prototype,t.prototype=new n;for(var r in i)t.prototype[r]=i[r];t.prototype.constructor=t,t.superClass=e}function h(t,e,n){a(t="prototype"in t?t.prototype:t,e="prototype"in e?e.prototype:e,n)}function c(t){if(t)return"string"!=typeof t&&"number"==typeof t.length}function d(t,e,n){if(t&&e)if(t.forEach&&t.forEach===Tp)t.forEach(e,n);else if(t.length===+t.length)for(var i=0,r=t.length;i<r;i++)e.call(n,t[i],i,t);else for(var o in t)t.hasOwnProperty(o)&&e.call(n,t[o],o,t)}function f(t,e,n){if(t&&e){if(t.map&&t.map===kp)return t.map(e,n);for(var i=[],r=0,o=t.length;r<o;r++)i.push(e.call(n,t[r],r,t));return i}}function p(t,e,n,i){if(t&&e){if(t.reduce&&t.reduce===Pp)return t.reduce(e,n,i);for(var r=0,o=t.length;r<o;r++)n=e.call(i,n,t[r],r,t);return n}}function g(t,e,n){if(t&&e){if(t.filter&&t.filter===Dp)return t.filter(e,n);for(var i=[],r=0,o=t.length;r<o;r++)e.call(n,t[r],r,t)&&i.push(t[r]);return i}}function m(t,e){var n=Ap.call(arguments,2);return function(){return t.apply(e,n.concat(Ap.call(arguments)))}}function v(t){var e=Ap.call(arguments,1);return function(){return t.apply(this,e.concat(Ap.call(arguments)))}}function y(t){return"[object Array]"===Ip.call(t)}function x(t){return"function"==typeof t}function _(t){return"[object String]"===Ip.call(t)}function w(t){var e=typeof t;return"function"===e||!!t&&"object"==e}function b(t){return!!Mp[Ip.call(t)]}function M(t){return!!Sp[Ip.call(t)]}function S(t){return"object"==typeof t&&"number"==typeof t.nodeType&&"object"==typeof t.ownerDocument}function I(t){return t!==t}function C(t){for(var e=0,n=arguments.length;e<n;e++)if(null!=arguments[e])return arguments[e]}function T(t,e){return null!=t?t:e}function D(t,e,n){return null!=t?t:null!=e?e:n}function A(){return Function.call.apply(Ap,arguments)}function k(t){if("number"==typeof t)return[t,t,t,t];var e=t.length;return 2===e?[t[0],t[1],t[0],t[1]]:3===e?[t[0],t[1],t[2],t[1]]:t}function P(t,e){if(!t)throw new Error(e)}function L(t){return null==t?null:"function"==typeof t.trim?t.trim():t.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}function O(t){t[Ep]=!0}function z(t){return t[Ep]}function E(t){function e(t,e){n?i.set(t,e):i.set(e,t)}var n=y(t),i=this;t instanceof E?t.each(e):t&&d(t,e)}function N(t){return new E(t)}function R(){}function B(t,e){var n=new Rp(2);return null==t&&(t=0),null==e&&(e=0),n[0]=t,n[1]=e,n}function V(t,e){return t[0]=e[0],t[1]=e[1],t}function F(t){var e=new Rp(2);return e[0]=t[0],e[1]=t[1],e}function H(t,e,n){return t[0]=e[0]+n[0],t[1]=e[1]+n[1],t}function G(t,e,n,i){return t[0]=e[0]+n[0]*i,t[1]=e[1]+n[1]*i,t}function W(t,e,n){return t[0]=e[0]-n[0],t[1]=e[1]-n[1],t}function Z(t){return Math.sqrt(U(t))}function U(t){return t[0]*t[0]+t[1]*t[1]}function X(t,e,n){return t[0]=e[0]*n,t[1]=e[1]*n,t}function j(t,e){var n=Z(e);return 0===n?(t[0]=0,t[1]=0):(t[0]=e[0]/n,t[1]=e[1]/n),t}function Y(t,e){return Math.sqrt((t[0]-e[0])*(t[0]-e[0])+(t[1]-e[1])*(t[1]-e[1]))}function q(t,e){return(t[0]-e[0])*(t[0]-e[0])+(t[1]-e[1])*(t[1]-e[1])}function $(t,e,n){var i=e[0],r=e[1];return t[0]=n[0]*i+n[2]*r+n[4],t[1]=n[1]*i+n[3]*r+n[5],t}function K(t,e,n){return t[0]=Math.min(e[0],n[0]),t[1]=Math.min(e[1],n[1]),t}function Q(t,e,n){return t[0]=Math.max(e[0],n[0]),t[1]=Math.max(e[1],n[1]),t}function J(){this.on("mousedown",this._dragStart,this),this.on("mousemove",this._drag,this),this.on("mouseup",this._dragEnd,this),this.on("globalout",this._dragEnd,this)}function tt(t,e){return{target:t,topTarget:e&&e.topTarget}}function et(t,e,n){return{type:t,event:n,target:e.target,topTarget:e.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which}}function nt(){}function it(t,e,n){if(t[t.rectHover?"rectContain":"contain"](e,n)){for(var i,r=t;r;){if(r.clipPath&&!r.clipPath.contain(e,n))return!1;r.silent&&(i=!0),r=r.parent}return!i||Up}return!1}function rt(){var t=new Yp(6);return ot(t),t}function ot(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t}function at(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t}function st(t,e,n){var i=e[0]*n[0]+e[2]*n[1],r=e[1]*n[0]+e[3]*n[1],o=e[0]*n[2]+e[2]*n[3],a=e[1]*n[2]+e[3]*n[3],s=e[0]*n[4]+e[2]*n[5]+e[4],l=e[1]*n[4]+e[3]*n[5]+e[5];return t[0]=i,t[1]=r,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t}function lt(t,e,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4]+n[0],t[5]=e[5]+n[1],t}function ut(t,e,n){var i=e[0],r=e[2],o=e[4],a=e[1],s=e[3],l=e[5],u=Math.sin(n),h=Math.cos(n);return t[0]=i*h+a*u,t[1]=-i*u+a*h,t[2]=r*h+s*u,t[3]=-r*u+h*s,t[4]=h*o+u*l,t[5]=h*l-u*o,t}function ht(t,e,n){var i=n[0],r=n[1];return t[0]=e[0]*i,t[1]=e[1]*r,t[2]=e[2]*i,t[3]=e[3]*r,t[4]=e[4]*i,t[5]=e[5]*r,t}function ct(t,e){var n=e[0],i=e[2],r=e[4],o=e[1],a=e[3],s=e[5],l=n*a-o*i;return l?(l=1/l,t[0]=a*l,t[1]=-o*l,t[2]=-i*l,t[3]=n*l,t[4]=(i*s-a*r)*l,t[5]=(o*r-n*s)*l,t):null}function dt(t){return t>Kp||t<-Kp}function ft(t){this._target=t.target,this._life=t.life||1e3,this._delay=t.delay||0,this._initialized=!1,this.loop=null!=t.loop&&t.loop,this.gap=t.gap||0,this.easing=t.easing||"Linear",this.onframe=t.onframe,this.ondestroy=t.ondestroy,this.onrestart=t.onrestart,this._pausedTime=0,this._paused=!1}function pt(t){return(t=Math.round(t))<0?0:t>255?255:t}function gt(t){return(t=Math.round(t))<0?0:t>360?360:t}function mt(t){return t<0?0:t>1?1:t}function vt(t){return pt(t.length&&"%"===t.charAt(t.length-1)?parseFloat(t)/100*255:parseInt(t,10))}function yt(t){return mt(t.length&&"%"===t.charAt(t.length-1)?parseFloat(t)/100:parseFloat(t))}function xt(t,e,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?t+(e-t)*n*6:2*n<1?e:3*n<2?t+(e-t)*(2/3-n)*6:t}function _t(t,e,n){return t+(e-t)*n}function wt(t,e,n,i,r){return t[0]=e,t[1]=n,t[2]=i,t[3]=r,t}function bt(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}function Mt(t,e){ug&&bt(ug,e),ug=lg.put(t,ug||e.slice())}function St(t,e){if(t){e=e||[];var n=lg.get(t);if(n)return bt(e,n);var i=(t+="").replace(/ /g,"").toLowerCase();if(i in sg)return bt(e,sg[i]),Mt(t,e),e;if("#"!==i.charAt(0)){var r=i.indexOf("("),o=i.indexOf(")");if(-1!==r&&o+1===i.length){var a=i.substr(0,r),s=i.substr(r+1,o-(r+1)).split(","),l=1;switch(a){case"rgba":if(4!==s.length)return void wt(e,0,0,0,1);l=yt(s.pop());case"rgb":return 3!==s.length?void wt(e,0,0,0,1):(wt(e,vt(s[0]),vt(s[1]),vt(s[2]),l),Mt(t,e),e);case"hsla":return 4!==s.length?void wt(e,0,0,0,1):(s[3]=yt(s[3]),It(s,e),Mt(t,e),e);case"hsl":return 3!==s.length?void wt(e,0,0,0,1):(It(s,e),Mt(t,e),e);default:return}}wt(e,0,0,0,1)}else{if(4===i.length)return(u=parseInt(i.substr(1),16))>=0&&u<=4095?(wt(e,(3840&u)>>4|(3840&u)>>8,240&u|(240&u)>>4,15&u|(15&u)<<4,1),Mt(t,e),e):void wt(e,0,0,0,1);if(7===i.length){var u=parseInt(i.substr(1),16);return u>=0&&u<=16777215?(wt(e,(16711680&u)>>16,(65280&u)>>8,255&u,1),Mt(t,e),e):void wt(e,0,0,0,1)}}}}function It(t,e){var n=(parseFloat(t[0])%360+360)%360/360,i=yt(t[1]),r=yt(t[2]),o=r<=.5?r*(i+1):r+i-r*i,a=2*r-o;return e=e||[],wt(e,pt(255*xt(a,o,n+1/3)),pt(255*xt(a,o,n)),pt(255*xt(a,o,n-1/3)),1),4===t.length&&(e[3]=t[3]),e}function Ct(t){if(t){var e,n,i=t[0]/255,r=t[1]/255,o=t[2]/255,a=Math.min(i,r,o),s=Math.max(i,r,o),l=s-a,u=(s+a)/2;if(0===l)e=0,n=0;else{n=u<.5?l/(s+a):l/(2-s-a);var h=((s-i)/6+l/2)/l,c=((s-r)/6+l/2)/l,d=((s-o)/6+l/2)/l;i===s?e=d-c:r===s?e=1/3+h-d:o===s&&(e=2/3+c-h),e<0&&(e+=1),e>1&&(e-=1)}var f=[360*e,n,u];return null!=t[3]&&f.push(t[3]),f}}function Tt(t,e){var n=St(t);if(n){for(var i=0;i<3;i++)n[i]=e<0?n[i]*(1-e)|0:(255-n[i])*e+n[i]|0,n[i]>255?n[i]=255:t[i]<0&&(n[i]=0);return Lt(n,4===n.length?"rgba":"rgb")}}function Dt(t){var e=St(t);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)}function At(t,e,n){if(e&&e.length&&t>=0&&t<=1){n=n||[];var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=e[r],s=e[o],l=i-r;return n[0]=pt(_t(a[0],s[0],l)),n[1]=pt(_t(a[1],s[1],l)),n[2]=pt(_t(a[2],s[2],l)),n[3]=mt(_t(a[3],s[3],l)),n}}function kt(t,e,n){if(e&&e.length&&t>=0&&t<=1){var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=St(e[r]),s=St(e[o]),l=i-r,u=Lt([pt(_t(a[0],s[0],l)),pt(_t(a[1],s[1],l)),pt(_t(a[2],s[2],l)),mt(_t(a[3],s[3],l))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:i}:u}}function Pt(t,e){if((t=St(t))&&null!=e)return t[3]=mt(e),Lt(t,"rgba")}function Lt(t,e){if(t&&t.length){var n=t[0]+","+t[1]+","+t[2];return"rgba"!==e&&"hsva"!==e&&"hsla"!==e||(n+=","+t[3]),e+"("+n+")"}}function Ot(t,e){return t[e]}function zt(t,e,n){t[e]=n}function Et(t,e,n){return(e-t)*n+t}function Nt(t,e,n){return n>.5?e:t}function Rt(t,e,n,i,r){var o=t.length;if(1==r)for(s=0;s<o;s++)i[s]=Et(t[s],e[s],n);else for(var a=o&&t[0].length,s=0;s<o;s++)for(var l=0;l<a;l++)i[s][l]=Et(t[s][l],e[s][l],n)}function Bt(t,e,n){var i=t.length,r=e.length;if(i!==r)if(i>r)t.length=r;else for(a=i;a<r;a++)t.push(1===n?e[a]:fg.call(e[a]));for(var o=t[0]&&t[0].length,a=0;a<t.length;a++)if(1===n)isNaN(t[a])&&(t[a]=e[a]);else for(var s=0;s<o;s++)isNaN(t[a][s])&&(t[a][s]=e[a][s])}function Vt(t,e,n){if(t===e)return!0;var i=t.length;if(i!==e.length)return!1;if(1===n){for(o=0;o<i;o++)if(t[o]!==e[o])return!1}else for(var r=t[0].length,o=0;o<i;o++)for(var a=0;a<r;a++)if(t[o][a]!==e[o][a])return!1;return!0}function Ft(t,e,n,i,r,o,a,s,l){var u=t.length;if(1==l)for(c=0;c<u;c++)s[c]=Ht(t[c],e[c],n[c],i[c],r,o,a);else for(var h=t[0].length,c=0;c<u;c++)for(var d=0;d<h;d++)s[c][d]=Ht(t[c][d],e[c][d],n[c][d],i[c][d],r,o,a)}function Ht(t,e,n,i,r,o,a){var s=.5*(n-t),l=.5*(i-e);return(2*(e-n)+s+l)*a+(-3*(e-n)-2*s-l)*o+s*r+e}function Gt(t){if(c(t)){var e=t.length;if(c(t[0])){for(var n=[],i=0;i<e;i++)n.push(fg.call(t[i]));return n}return fg.call(t)}return t}function Wt(t){return t[0]=Math.floor(t[0]),t[1]=Math.floor(t[1]),t[2]=Math.floor(t[2]),"rgba("+t.join(",")+")"}function Zt(t){var e=t[t.length-1].value;return c(e&&e[0])?2:1}function Ut(t,e,n,i,r,o){var a=t._getter,s=t._setter,l="spline"===e,u=i.length;if(u){var h,d=c(i[0].value),f=!1,p=!1,g=d?Zt(i):0;i.sort(function(t,e){return t.time-e.time}),h=i[u-1].time;for(var m=[],v=[],y=i[0].value,x=!0,_=0;_<u;_++){m.push(i[_].time/h);var w=i[_].value;if(d&&Vt(w,y,g)||!d&&w===y||(x=!1),y=w,"string"==typeof w){var b=St(w);b?(w=b,f=!0):p=!0}v.push(w)}if(o||!x){for(var M=v[u-1],_=0;_<u-1;_++)d?Bt(v[_],M,g):!isNaN(v[_])||isNaN(M)||p||f||(v[_]=M);d&&Bt(a(t._target,r),M,g);var S,I,C,T,D,A,k=0,P=0;if(f)var L=[0,0,0,0];var O=new ft({target:t._target,life:h,loop:t._loop,delay:t._delay,onframe:function(t,e){var n;if(e<0)n=0;else if(e<P){for(n=S=Math.min(k+1,u-1);n>=0&&!(m[n]<=e);n--);n=Math.min(n,u-2)}else{for(n=k;n<u&&!(m[n]>e);n++);n=Math.min(n-1,u-2)}k=n,P=e;var i=m[n+1]-m[n];if(0!==i)if(I=(e-m[n])/i,l)if(T=v[n],C=v[0===n?n:n-1],D=v[n>u-2?u-1:n+1],A=v[n>u-3?u-1:n+2],d)Ft(C,T,D,A,I,I*I,I*I*I,a(t,r),g);else{if(f)o=Ft(C,T,D,A,I,I*I,I*I*I,L,1),o=Wt(L);else{if(p)return Nt(T,D,I);o=Ht(C,T,D,A,I,I*I,I*I*I)}s(t,r,o)}else if(d)Rt(v[n],v[n+1],I,a(t,r),g);else{var o;if(f)Rt(v[n],v[n+1],I,L,1),o=Wt(L);else{if(p)return Nt(v[n],v[n+1],I);o=Et(v[n],v[n+1],I)}s(t,r,o)}},ondestroy:n});return e&&"spline"!==e&&(O.easing=e),O}}}function Xt(t,e,n,i){n<0&&(t+=n,n=-n),i<0&&(e+=i,i=-i),this.x=t,this.y=e,this.width=n,this.height=i}function jt(t){for(var e=0;t>=Ig;)e|=1&t,t>>=1;return t+e}function Yt(t,e,n,i){var r=e+1;if(r===n)return 1;if(i(t[r++],t[e])<0){for(;r<n&&i(t[r],t[r-1])<0;)r++;qt(t,e,r)}else for(;r<n&&i(t[r],t[r-1])>=0;)r++;return r-e}function qt(t,e,n){for(n--;e<n;){var i=t[e];t[e++]=t[n],t[n--]=i}}function $t(t,e,n,i,r){for(i===e&&i++;i<n;i++){for(var o,a=t[i],s=e,l=i;s<l;)r(a,t[o=s+l>>>1])<0?l=o:s=o+1;var u=i-s;switch(u){case 3:t[s+3]=t[s+2];case 2:t[s+2]=t[s+1];case 1:t[s+1]=t[s];break;default:for(;u>0;)t[s+u]=t[s+u-1],u--}t[s]=a}}function Kt(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])>0){for(s=i-r;l<s&&o(t,e[n+r+l])>0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;l<s&&o(t,e[n+r-l])<=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a<l;){var h=a+(l-a>>>1);o(t,e[n+h])>0?a=h+1:l=h}return l}function Qt(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])<0){for(s=r+1;l<s&&o(t,e[n+r-l])<0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l<s&&o(t,e[n+r+l])>=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a<l;){var h=a+(l-a>>>1);o(t,e[n+h])<0?l=h:a=h+1}return l}function Jt(t,e){function n(n){var s=o[n],u=a[n],h=o[n+1],c=a[n+1];a[n]=u+c,n===l-3&&(o[n+1]=o[n+2],a[n+1]=a[n+2]),l--;var d=Qt(t[h],t,s,u,0,e);s+=d,0!==(u-=d)&&0!==(c=Kt(t[s+u-1],t,h,c,c-1,e))&&(u<=c?i(s,u,h,c):r(s,u,h,c))}function i(n,i,r,o){var a=0;for(a=0;a<i;a++)u[a]=t[n+a];var l=0,h=r,c=n;if(t[c++]=t[h++],0!=--o)if(1!==i){for(var d,f,p,g=s;;){d=0,f=0,p=!1;do{if(e(t[h],u[l])<0){if(t[c++]=t[h++],f++,d=0,0==--o){p=!0;break}}else if(t[c++]=u[l++],d++,f=0,1==--i){p=!0;break}}while((d|f)<g);if(p)break;do{if(0!==(d=Qt(t[h],u,l,i,0,e))){for(a=0;a<d;a++)t[c+a]=u[l+a];if(c+=d,l+=d,(i-=d)<=1){p=!0;break}}if(t[c++]=t[h++],0==--o){p=!0;break}if(0!==(f=Kt(u[l],t,h,o,0,e))){for(a=0;a<f;a++)t[c+a]=t[h+a];if(c+=f,h+=f,0===(o-=f)){p=!0;break}}if(t[c++]=u[l++],1==--i){p=!0;break}g--}while(d>=Cg||f>=Cg);if(p)break;g<0&&(g=0),g+=2}if((s=g)<1&&(s=1),1===i){for(a=0;a<o;a++)t[c+a]=t[h+a];t[c+o]=u[l]}else{if(0===i)throw new Error;for(a=0;a<i;a++)t[c+a]=u[l+a]}}else{for(a=0;a<o;a++)t[c+a]=t[h+a];t[c+o]=u[l]}else for(a=0;a<i;a++)t[c+a]=u[l+a]}function r(n,i,r,o){var a=0;for(a=0;a<o;a++)u[a]=t[r+a];var l=n+i-1,h=o-1,c=r+o-1,d=0,f=0;if(t[c--]=t[l--],0!=--i)if(1!==o){for(var p=s;;){var g=0,m=0,v=!1;do{if(e(u[h],t[l])<0){if(t[c--]=t[l--],g++,m=0,0==--i){v=!0;break}}else if(t[c--]=u[h--],m++,g=0,1==--o){v=!0;break}}while((g|m)<p);if(v)break;do{if(0!=(g=i-Qt(u[h],t,n,i,i-1,e))){for(i-=g,f=(c-=g)+1,d=(l-=g)+1,a=g-1;a>=0;a--)t[f+a]=t[d+a];if(0===i){v=!0;break}}if(t[c--]=u[h--],1==--o){v=!0;break}if(0!=(m=o-Kt(t[l],u,0,o,o-1,e))){for(o-=m,f=(c-=m)+1,d=(h-=m)+1,a=0;a<m;a++)t[f+a]=u[d+a];if(o<=1){v=!0;break}}if(t[c--]=t[l--],0==--i){v=!0;break}p--}while(g>=Cg||m>=Cg);if(v)break;p<0&&(p=0),p+=2}if((s=p)<1&&(s=1),1===o){for(f=(c-=i)+1,d=(l-=i)+1,a=i-1;a>=0;a--)t[f+a]=t[d+a];t[c]=u[h]}else{if(0===o)throw new Error;for(d=c-(o-1),a=0;a<o;a++)t[d+a]=u[a]}}else{for(f=(c-=i)+1,d=(l-=i)+1,a=i-1;a>=0;a--)t[f+a]=t[d+a];t[c]=u[h]}else for(d=c-(o-1),a=0;a<o;a++)t[d+a]=u[a]}var o,a,s=Cg,l=0,u=[];o=[],a=[],this.mergeRuns=function(){for(;l>1;){var t=l-2;if(t>=1&&a[t-1]<=a[t]+a[t+1]||t>=2&&a[t-2]<=a[t]+a[t-1])a[t-1]<a[t+1]&&t--;else if(a[t]>a[t+1])break;n(t)}},this.forceMergeRuns=function(){for(;l>1;){var t=l-2;t>0&&a[t-1]<a[t+1]&&t--,n(t)}},this.pushRun=function(t,e){o[l]=t,a[l]=e,l+=1}}function te(t,e,n,i){n||(n=0),i||(i=t.length);var r=i-n;if(!(r<2)){var o=0;if(r<Ig)return o=Yt(t,n,i,e),void $t(t,n,i,n+o,e);var a=new Jt(t,e),s=jt(r);do{if((o=Yt(t,n,i,e))<s){var l=r;l>s&&(l=s),$t(t,n,n+l,n+o,e),o=l}a.pushRun(n,o),a.mergeRuns(),r-=o,n+=o}while(0!==r);a.forceMergeRuns()}}function ee(t,e){return t.zlevel===e.zlevel?t.z===e.z?t.z2-e.z2:t.z-e.z:t.zlevel-e.zlevel}function ne(t,e,n){var i=null==e.x?0:e.x,r=null==e.x2?1:e.x2,o=null==e.y?0:e.y,a=null==e.y2?0:e.y2;return e.global||(i=i*n.width+n.x,r=r*n.width+n.x,o=o*n.height+n.y,a=a*n.height+n.y),i=isNaN(i)?0:i,r=isNaN(r)?1:r,o=isNaN(o)?0:o,a=isNaN(a)?0:a,t.createLinearGradient(i,o,r,a)}function ie(t,e,n){var i=n.width,r=n.height,o=Math.min(i,r),a=null==e.x?.5:e.x,s=null==e.y?.5:e.y,l=null==e.r?.5:e.r;return e.global||(a=a*i+n.x,s=s*r+n.y,l*=o),t.createRadialGradient(a,s,0,a,s,l)}function re(){return!1}function oe(t,e,n){var i=Op(),r=e.getWidth(),o=e.getHeight(),a=i.style;return a&&(a.position="absolute",a.left=0,a.top=0,a.width=r+"px",a.height=o+"px",i.setAttribute("data-zr-dom-id",t)),i.width=r*n,i.height=o*n,i}function ae(t){if("string"==typeof t){var e=Bg.get(t);return e&&e.image}return t}function se(t,e,n,i,r){if(t){if("string"==typeof t){if(e&&e.__zrImageSrc===t||!n)return e;var o=Bg.get(t),a={hostEl:n,cb:i,cbPayload:r};return o?!ue(e=o.image)&&o.pending.push(a):(!e&&(e=new Image),e.onload=le,Bg.put(t,e.__cachedImgObj={image:e,pending:[a]}),e.src=e.__zrImageSrc=t),e}return t}return e}function le(){var t=this.__cachedImgObj;this.onload=this.__cachedImgObj=null;for(var e=0;e<t.pending.length;e++){var n=t.pending[e],i=n.cb;i&&i(this,n.cbPayload),n.hostEl.dirty()}t.pending.length=0}function ue(t){return t&&t.width&&t.height}function he(t,e){var n=t+":"+(e=e||Wg);if(Vg[n])return Vg[n];for(var i=(t+"").split("\n"),r=0,o=0,a=i.length;o<a;o++)r=Math.max(be(i[o],e).width,r);return Fg>Hg&&(Fg=0,Vg={}),Fg++,Vg[n]=r,r}function ce(t,e,n,i,r,o,a){return o?fe(t,e,n,i,r,o,a):de(t,e,n,i,r,a)}function de(t,e,n,i,r,o){var a=Me(t,e,r,o),s=he(t,e);r&&(s+=r[1]+r[3]);var l=a.outerHeight,u=new Xt(pe(0,s,n),ge(0,l,i),s,l);return u.lineHeight=a.lineHeight,u}function fe(t,e,n,i,r,o,a){var s=Se(t,{rich:o,truncate:a,font:e,textAlign:n,textPadding:r}),l=s.outerWidth,u=s.outerHeight;return new Xt(pe(0,l,n),ge(0,u,i),l,u)}function pe(t,e,n){return"right"===n?t-=e:"center"===n&&(t-=e/2),t}function ge(t,e,n){return"middle"===n?t-=e/2:"bottom"===n&&(t-=e),t}function me(t,e,n){var i=e.x,r=e.y,o=e.height,a=e.width,s=o/2,l="left",u="top";switch(t){case"left":i-=n,r+=s,l="right",u="middle";break;case"right":i+=n+a,r+=s,u="middle";break;case"top":i+=a/2,r-=n,l="center",u="bottom";break;case"bottom":i+=a/2,r+=o+n,l="center";break;case"inside":i+=a/2,r+=s,l="center",u="middle";break;case"insideLeft":i+=n,r+=s,u="middle";break;case"insideRight":i+=a-n,r+=s,l="right",u="middle";break;case"insideTop":i+=a/2,r+=n,l="center";break;case"insideBottom":i+=a/2,r+=o-n,l="center",u="bottom";break;case"insideTopLeft":i+=n,r+=n;break;case"insideTopRight":i+=a-n,r+=n,l="right";break;case"insideBottomLeft":i+=n,r+=o-n,u="bottom";break;case"insideBottomRight":i+=a-n,r+=o-n,l="right",u="bottom"}return{x:i,y:r,textAlign:l,textVerticalAlign:u}}function ve(t,e,n,i,r){if(!e)return"";var o=(t+"").split("\n");r=ye(e,n,i,r);for(var a=0,s=o.length;a<s;a++)o[a]=xe(o[a],r);return o.join("\n")}function ye(t,e,n,i){(i=o({},i)).font=e;var n=T(n,"...");i.maxIterations=T(i.maxIterations,2);var r=i.minChar=T(i.minChar,0);i.cnCharWidth=he("国",e);var a=i.ascCharWidth=he("a",e);i.placeholder=T(i.placeholder,"");for(var s=t=Math.max(0,t-1),l=0;l<r&&s>=a;l++)s-=a;var u=he(n);return u>s&&(n="",u=0),s=t-u,i.ellipsis=n,i.ellipsisWidth=u,i.contentWidth=s,i.containerWidth=t,i}function xe(t,e){var n=e.containerWidth,i=e.font,r=e.contentWidth;if(!n)return"";var o=he(t,i);if(o<=n)return t;for(var a=0;;a++){if(o<=r||a>=e.maxIterations){t+=e.ellipsis;break}var s=0===a?_e(t,r,e.ascCharWidth,e.cnCharWidth):o>0?Math.floor(t.length*r/o):0;o=he(t=t.substr(0,s),i)}return""===t&&(t=e.placeholder),t}function _e(t,e,n,i){for(var r=0,o=0,a=t.length;o<a&&r<e;o++){var s=t.charCodeAt(o);r+=0<=s&&s<=127?n:i}return o}function we(t){return he("国",t)}function be(t,e){return Zg.measureText(t,e)}function Me(t,e,n,i){null!=t&&(t+="");var r=we(e),o=t?t.split("\n"):[],a=o.length*r,s=a;if(n&&(s+=n[0]+n[2]),t&&i){var l=i.outerHeight,u=i.outerWidth;if(null!=l&&s>l)t="",o=[];else if(null!=u)for(var h=ye(u-(n?n[1]+n[3]:0),e,i.ellipsis,{minChar:i.minChar,placeholder:i.placeholder}),c=0,d=o.length;c<d;c++)o[c]=xe(o[c],h)}return{lines:o,height:a,outerHeight:s,lineHeight:r}}function Se(t,e){var n={lines:[],width:0,height:0};if(null!=t&&(t+=""),!t)return n;for(var i,r=Gg.lastIndex=0;null!=(i=Gg.exec(t));){var o=i.index;o>r&&Ie(n,t.substring(r,o)),Ie(n,i[2],i[1]),r=Gg.lastIndex}r<t.length&&Ie(n,t.substring(r,t.length));var a=n.lines,s=0,l=0,u=[],h=e.textPadding,c=e.truncate,d=c&&c.outerWidth,f=c&&c.outerHeight;h&&(null!=d&&(d-=h[1]+h[3]),null!=f&&(f-=h[0]+h[2]));for(k=0;k<a.length;k++){for(var p=a[k],g=0,m=0,v=0;v<p.tokens.length;v++){var y=(P=p.tokens[v]).styleName&&e.rich[P.styleName]||{},x=P.textPadding=y.textPadding,_=P.font=y.font||e.font,w=P.textHeight=T(y.textHeight,we(_));if(x&&(w+=x[0]+x[2]),P.height=w,P.lineHeight=D(y.textLineHeight,e.textLineHeight,w),P.textAlign=y&&y.textAlign||e.textAlign,P.textVerticalAlign=y&&y.textVerticalAlign||"middle",null!=f&&s+P.lineHeight>f)return{lines:[],width:0,height:0};P.textWidth=he(P.text,_);var b=y.textWidth,M=null==b||"auto"===b;if("string"==typeof b&&"%"===b.charAt(b.length-1))P.percentWidth=b,u.push(P),b=0;else{if(M){b=P.textWidth;var S=y.textBackgroundColor,I=S&&S.image;I&&ue(I=ae(I))&&(b=Math.max(b,I.width*w/I.height))}var C=x?x[1]+x[3]:0;b+=C;var A=null!=d?d-m:null;null!=A&&A<b&&(!M||A<C?(P.text="",P.textWidth=b=0):(P.text=ve(P.text,A-C,_,c.ellipsis,{minChar:c.minChar}),P.textWidth=he(P.text,_),b=P.textWidth+C))}m+=P.width=b,y&&(g=Math.max(g,P.lineHeight))}p.width=m,p.lineHeight=g,s+=g,l=Math.max(l,m)}n.outerWidth=n.width=T(e.textWidth,l),n.outerHeight=n.height=T(e.textHeight,s),h&&(n.outerWidth+=h[1]+h[3],n.outerHeight+=h[0]+h[2]);for(var k=0;k<u.length;k++){var P=u[k],L=P.percentWidth;P.width=parseInt(L,10)/100*l}return n}function Ie(t,e,n){for(var i=""===e,r=e.split("\n"),o=t.lines,a=0;a<r.length;a++){var s=r[a],l={styleName:n,text:s,isLineHolder:!s&&!i};if(a)o.push({tokens:[l]});else{var u=(o[o.length-1]||(o[0]={tokens:[]})).tokens,h=u.length;1===h&&u[0].isLineHolder?u[0]=l:(s||!h||i)&&u.push(l)}}}function Ce(t){var e=(t.fontSize||t.fontFamily)&&[t.fontStyle,t.fontWeight,(t.fontSize||12)+"px",t.fontFamily||"sans-serif"].join(" ");return e&&L(e)||t.textFont||t.font}function Te(t,e){var n,i,r,o,a=e.x,s=e.y,l=e.width,u=e.height,h=e.r;l<0&&(a+=l,l=-l),u<0&&(s+=u,u=-u),"number"==typeof h?n=i=r=o=h:h instanceof Array?1===h.length?n=i=r=o=h[0]:2===h.length?(n=r=h[0],i=o=h[1]):3===h.length?(n=h[0],i=o=h[1],r=h[2]):(n=h[0],i=h[1],r=h[2],o=h[3]):n=i=r=o=0;var c;n+i>l&&(n*=l/(c=n+i),i*=l/c),r+o>l&&(r*=l/(c=r+o),o*=l/c),i+r>u&&(i*=u/(c=i+r),r*=u/c),n+o>u&&(n*=u/(c=n+o),o*=u/c),t.moveTo(a+n,s),t.lineTo(a+l-i,s),0!==i&&t.arc(a+l-i,s+i,i,-Math.PI/2,0),t.lineTo(a+l,s+u-r),0!==r&&t.arc(a+l-r,s+u-r,r,0,Math.PI/2),t.lineTo(a+o,s+u),0!==o&&t.arc(a+o,s+u-o,o,Math.PI/2,Math.PI),t.lineTo(a,s+n),0!==n&&t.arc(a+n,s+n,n,Math.PI,1.5*Math.PI)}function De(t){return Ae(t),d(t.rich,Ae),t}function Ae(t){if(t){t.font=Ce(t);var e=t.textAlign;"middle"===e&&(e="center"),t.textAlign=null==e||Ug[e]?e:"left";var n=t.textVerticalAlign||t.textBaseline;"center"===n&&(n="middle"),t.textVerticalAlign=null==n||Xg[n]?n:"top",t.textPadding&&(t.textPadding=k(t.textPadding))}}function ke(t,e,n,i,r){i.rich?Le(t,e,n,i,r):Pe(t,e,n,i,r)}function Pe(t,e,n,i,r){var o=Fe(e,"font",i.font||Wg),a=i.textPadding,s=t.__textCotentBlock;s&&!t.__dirty||(s=t.__textCotentBlock=Me(n,o,a,i.truncate));var l=s.outerHeight,u=s.lines,h=s.lineHeight,c=Ve(l,i,r),d=c.baseX,f=c.baseY,p=c.textAlign,g=c.textVerticalAlign;ze(e,i,r,d,f);var m=ge(f,l,g),v=d,y=m,x=Ne(i);if(x||a){var _=he(n,o);a&&(_+=a[1]+a[3]);var w=pe(d,_,p);x&&Re(t,e,i,w,m,_,l),a&&(v=Ze(d,p,a),y+=a[0])}Fe(e,"textAlign",p||"left"),Fe(e,"textBaseline","middle"),Fe(e,"shadowBlur",i.textShadowBlur||0),Fe(e,"shadowColor",i.textShadowColor||"transparent"),Fe(e,"shadowOffsetX",i.textShadowOffsetX||0),Fe(e,"shadowOffsetY",i.textShadowOffsetY||0),y+=h/2;var b=i.textStrokeWidth,M=He(i.textStroke,b),S=Ge(i.textFill);M&&(Fe(e,"lineWidth",b),Fe(e,"strokeStyle",M)),S&&Fe(e,"fillStyle",S);for(var I=0;I<u.length;I++)M&&e.strokeText(u[I],v,y),S&&e.fillText(u[I],v,y),y+=h}function Le(t,e,n,i,r){var o=t.__textCotentBlock;o&&!t.__dirty||(o=t.__textCotentBlock=Se(n,i)),Oe(t,e,o,i,r)}function Oe(t,e,n,i,r){var o=n.width,a=n.outerWidth,s=n.outerHeight,l=i.textPadding,u=Ve(s,i,r),h=u.baseX,c=u.baseY,d=u.textAlign,f=u.textVerticalAlign;ze(e,i,r,h,c);var p=pe(h,a,d),g=ge(c,s,f),m=p,v=g;l&&(m+=l[3],v+=l[0]);var y=m+o;Ne(i)&&Re(t,e,i,p,g,a,s);for(var x=0;x<n.lines.length;x++){for(var _,w=n.lines[x],b=w.tokens,M=b.length,S=w.lineHeight,I=w.width,C=0,T=m,D=y,A=M-1;C<M&&(!(_=b[C]).textAlign||"left"===_.textAlign);)Ee(t,e,_,i,S,v,T,"left"),I-=_.width,T+=_.width,C++;for(;A>=0&&"right"===(_=b[A]).textAlign;)Ee(t,e,_,i,S,v,D,"right"),I-=_.width,D-=_.width,A--;for(T+=(o-(T-m)-(y-D)-I)/2;C<=A;)Ee(t,e,_=b[C],i,S,v,T+_.width/2,"center"),T+=_.width,C++;v+=S}}function ze(t,e,n,i,r){if(n&&e.textRotation){var o=e.textOrigin;"center"===o?(i=n.width/2+n.x,r=n.height/2+n.y):o&&(i=o[0]+n.x,r=o[1]+n.y),t.translate(i,r),t.rotate(-e.textRotation),t.translate(-i,-r)}}function Ee(t,e,n,i,r,o,a,s){var l=i.rich[n.styleName]||{},u=n.textVerticalAlign,h=o+r/2;"top"===u?h=o+n.height/2:"bottom"===u&&(h=o+r-n.height/2),!n.isLineHolder&&Ne(l)&&Re(t,e,l,"right"===s?a-n.width:"center"===s?a-n.width/2:a,h-n.height/2,n.width,n.height);var c=n.textPadding;c&&(a=Ze(a,s,c),h-=n.height/2-c[2]-n.textHeight/2),Fe(e,"shadowBlur",D(l.textShadowBlur,i.textShadowBlur,0)),Fe(e,"shadowColor",l.textShadowColor||i.textShadowColor||"transparent"),Fe(e,"shadowOffsetX",D(l.textShadowOffsetX,i.textShadowOffsetX,0)),Fe(e,"shadowOffsetY",D(l.textShadowOffsetY,i.textShadowOffsetY,0)),Fe(e,"textAlign",s),Fe(e,"textBaseline","middle"),Fe(e,"font",n.font||Wg);var d=He(l.textStroke||i.textStroke,p),f=Ge(l.textFill||i.textFill),p=T(l.textStrokeWidth,i.textStrokeWidth);d&&(Fe(e,"lineWidth",p),Fe(e,"strokeStyle",d),e.strokeText(n.text,a,h)),f&&(Fe(e,"fillStyle",f),e.fillText(n.text,a,h))}function Ne(t){return t.textBackgroundColor||t.textBorderWidth&&t.textBorderColor}function Re(t,e,n,i,r,o,a){var s=n.textBackgroundColor,l=n.textBorderWidth,u=n.textBorderColor,h=_(s);if(Fe(e,"shadowBlur",n.textBoxShadowBlur||0),Fe(e,"shadowColor",n.textBoxShadowColor||"transparent"),Fe(e,"shadowOffsetX",n.textBoxShadowOffsetX||0),Fe(e,"shadowOffsetY",n.textBoxShadowOffsetY||0),h||l&&u){e.beginPath();var c=n.textBorderRadius;c?Te(e,{x:i,y:r,width:o,height:a,r:c}):e.rect(i,r,o,a),e.closePath()}if(h)Fe(e,"fillStyle",s),e.fill();else if(w(s)){var d=s.image;(d=se(d,null,t,Be,s))&&ue(d)&&e.drawImage(d,i,r,o,a)}l&&u&&(Fe(e,"lineWidth",l),Fe(e,"strokeStyle",u),e.stroke())}function Be(t,e){e.image=t}function Ve(t,e,n){var i=e.x||0,r=e.y||0,o=e.textAlign,a=e.textVerticalAlign;if(n){var s=e.textPosition;if(s instanceof Array)i=n.x+We(s[0],n.width),r=n.y+We(s[1],n.height);else{var l=me(s,n,e.textDistance);i=l.x,r=l.y,o=o||l.textAlign,a=a||l.textVerticalAlign}var u=e.textOffset;u&&(i+=u[0],r+=u[1])}return{baseX:i,baseY:r,textAlign:o,textVerticalAlign:a}}function Fe(t,e,n){return t[e]=Ag(t,e,n),t[e]}function He(t,e){return null==t||e<=0||"transparent"===t||"none"===t?null:t.image||t.colorStops?"#000":t}function Ge(t){return null==t||"none"===t?null:t.image||t.colorStops?"#000":t}function We(t,e){return"string"==typeof t?t.lastIndexOf("%")>=0?parseFloat(t)/100*e:parseFloat(t):t}function Ze(t,e,n){return"right"===e?t-n[1]:"center"===e?t+n[3]/2-n[1]/2:t+n[3]}function Ue(t,e){return null!=t&&(t||e.textBackgroundColor||e.textBorderWidth&&e.textBorderColor||e.textPadding)}function Xe(t){t=t||{},_g.call(this,t);for(var e in t)t.hasOwnProperty(e)&&"style"!==e&&(this[e]=t[e]);this.style=new Pg(t.style,this),this._rect=null,this.__clipPaths=[]}function je(t){Xe.call(this,t)}function Ye(t){return parseInt(t,10)}function qe(t){return!!t&&(!!t.__builtin__||"function"==typeof t.resize&&"function"==typeof t.refresh)}function $e(t,e,n){return qg.copy(t.getBoundingRect()),t.transform&&qg.applyTransform(t.transform),$g.width=e,$g.height=n,!qg.intersect($g)}function Ke(t,e){if(t==e)return!1;if(!t||!e||t.length!==e.length)return!0;for(var n=0;n<t.length;n++)if(t[n]!==e[n])return!0}function Qe(t,e){for(var n=0;n<t.length;n++){var i=t[n];i.setTransform(e),e.beginPath(),i.buildPath(e,i.shape),e.clip(),i.restoreTransform(e)}}function Je(t,e){var n=document.createElement("div");return n.style.cssText=["position:relative","overflow:hidden","width:"+t+"px","height:"+e+"px","padding:0","margin:0","border-width:0"].join(";")+";",n}function tn(t){return t.getBoundingClientRect?t.getBoundingClientRect():{left:0,top:0}}function en(t,e,n,i){return n=n||{},i||!bp.canvasSupported?nn(t,e,n):bp.browser.firefox&&null!=e.layerX&&e.layerX!==e.offsetX?(n.zrX=e.layerX,n.zrY=e.layerY):null!=e.offsetX?(n.zrX=e.offsetX,n.zrY=e.offsetY):nn(t,e,n),n}function nn(t,e,n){var i=tn(t);n.zrX=e.clientX-i.left,n.zrY=e.clientY-i.top}function rn(t,e,n){if(null!=(e=e||window.event).zrX)return e;var i=e.type;if(i&&i.indexOf("touch")>=0){var r="touchend"!=i?e.targetTouches[0]:e.changedTouches[0];r&&en(t,r,e,n)}else en(t,e,e,n),e.zrDelta=e.wheelDelta?e.wheelDelta/120:-(e.detail||0)/3;var o=e.button;return null==e.which&&void 0!==o&&Jg.test(e.type)&&(e.which=1&o?1:2&o?3:4&o?2:0),e}function on(t,e,n){Qg?t.addEventListener(e,n):t.attachEvent("on"+e,n)}function an(t,e,n){Qg?t.removeEventListener(e,n):t.detachEvent("on"+e,n)}function sn(t){return t.which>1}function ln(t){var e=t[1][0]-t[0][0],n=t[1][1]-t[0][1];return Math.sqrt(e*e+n*n)}function un(t){return[(t[0][0]+t[1][0])/2,(t[0][1]+t[1][1])/2]}function hn(t){return"mousewheel"===t&&bp.browser.firefox?"DOMMouseScroll":t}function cn(t,e,n){var i=t._gestureMgr;"start"===n&&i.clear();var r=i.recognize(e,t.handler.findHover(e.zrX,e.zrY,null).target,t.dom);if("end"===n&&i.clear(),r){var o=r.type;e.gestureEvent=o,t.handler.dispatchToElement({target:r.target},o,r.event)}}function dn(t){t._touching=!0,clearTimeout(t._touchTimer),t._touchTimer=setTimeout(function(){t._touching=!1},700)}function fn(t){var e=t.pointerType;return"pen"===e||"touch"===e}function pn(t){function e(t,e){return function(){if(!e._touching)return t.apply(e,arguments)}}d(om,function(e){t._handlers[e]=m(lm[e],t)}),d(sm,function(e){t._handlers[e]=m(lm[e],t)}),d(rm,function(n){t._handlers[n]=e(lm[n],t)})}function gn(t){function e(e,n){d(e,function(e){on(t,hn(e),n._handlers[e])},n)}Zp.call(this),this.dom=t,this._touching=!1,this._touchTimer,this._gestureMgr=new nm,this._handlers={},pn(this),bp.pointerEventsSupported?e(sm,this):(bp.touchEventsSupported&&e(om,this),e(rm,this))}function mn(t,e){var n=new fm(_p(),t,e);return dm[n.id]=n,n}function vn(t,e){cm[t]=e}function yn(t){delete dm[t]}function xn(t){return t instanceof Array?t:null==t?[]:[t]}function _n(t,e,n){if(t){t[e]=t[e]||{},t.emphasis=t.emphasis||{},t.emphasis[e]=t.emphasis[e]||{};for(var i=0,r=n.length;i<r;i++){var o=n[i];!t.emphasis[e].hasOwnProperty(o)&&t[e].hasOwnProperty(o)&&(t.emphasis[e][o]=t[e][o])}}}function wn(t){return!mm(t)||vm(t)||t instanceof Date?t:t.value}function bn(t){return mm(t)&&!(t instanceof Array)}function Mn(t,e){e=(e||[]).slice();var n=f(t||[],function(t,e){return{exist:t}});return gm(e,function(t,i){if(mm(t)){for(r=0;r<n.length;r++)if(!n[r].option&&null!=t.id&&n[r].exist.id===t.id+"")return n[r].option=t,void(e[i]=null);for(var r=0;r<n.length;r++){var o=n[r].exist;if(!(n[r].option||null!=o.id&&null!=t.id||null==t.name||Cn(t)||Cn(o)||o.name!==t.name+""))return n[r].option=t,void(e[i]=null)}}}),gm(e,function(t,e){if(mm(t)){for(var i=0;i<n.length;i++){var r=n[i].exist;if(!n[i].option&&!Cn(r)&&null==t.id){n[i].option=t;break}}i>=n.length&&n.push({option:t})}}),n}function Sn(t){var e=N();gm(t,function(t,n){var i=t.exist;i&&e.set(i.id,t)}),gm(t,function(t,n){var i=t.option;P(!i||null==i.id||!e.get(i.id)||e.get(i.id)===t,"id duplicates: "+(i&&i.id)),i&&null!=i.id&&e.set(i.id,t),!t.keyInfo&&(t.keyInfo={})}),gm(t,function(t,n){var i=t.exist,r=t.option,o=t.keyInfo;if(mm(r)){if(o.name=null!=r.name?r.name+"":i?i.name:ym+n,i)o.id=i.id;else if(null!=r.id)o.id=r.id+"";else{var a=0;do{o.id="\0"+o.name+"\0"+a++}while(e.get(o.id))}e.set(o.id,t)}})}function In(t){var e=t.name;return!(!e||!e.indexOf(ym))}function Cn(t){return mm(t)&&t.id&&0===(t.id+"").indexOf("\0_ec_\0")}function Tn(t,e){return null!=e.dataIndexInside?e.dataIndexInside:null!=e.dataIndex?y(e.dataIndex)?f(e.dataIndex,function(e){return t.indexOfRawIndex(e)}):t.indexOfRawIndex(e.dataIndex):null!=e.name?y(e.name)?f(e.name,function(e){return t.indexOfName(e)}):t.indexOfName(e.name):void 0}function Dn(){var t="__\0ec_inner_"+_m+++"_"+Math.random().toFixed(5);return function(e){return e[t]||(e[t]={})}}function An(t,e,n){if(_(e)){var i={};i[e+"Index"]=0,e=i}var r=n&&n.defaultMainType;!r||kn(e,r+"Index")||kn(e,r+"Id")||kn(e,r+"Name")||(e[r+"Index"]=0);var o={};return gm(e,function(i,r){var i=e[r];if("dataIndex"!==r&&"dataIndexInside"!==r){var a=r.match(/^(\w+)(Index|Id|Name)$/)||[],s=a[1],u=(a[2]||"").toLowerCase();if(!(!s||!u||null==i||"index"===u&&"none"===i||n&&n.includeMainTypes&&l(n.includeMainTypes,s)<0)){var h={mainType:s};"index"===u&&"all"===i||(h[u]=i);var c=t.queryComponents(h);o[s+"Models"]=c,o[s+"Model"]=c[0]}}else o[r]=i}),o}function kn(t,e){return t&&t.hasOwnProperty(e)}function Pn(t,e,n){t.setAttribute?t.setAttribute(e,n):t[e]=n}function Ln(t,e){return t.getAttribute?t.getAttribute(e):t[e]}function On(t){var e={main:"",sub:""};return t&&(t=t.split(wm),e.main=t[0]||"",e.sub=t[1]||""),e}function zn(t){P(/^[a-zA-Z0-9_]+([.][a-zA-Z0-9_]+)?$/.test(t),'componentType "'+t+'" illegal')}function En(t,e){t.$constructor=t,t.extend=function(t){var e=this,n=function(){t.$constructor?t.$constructor.apply(this,arguments):e.apply(this,arguments)};return o(n.prototype,t),n.extend=this.extend,n.superCall=Rn,n.superApply=Bn,u(n,this),n.superClass=e,n}}function Nn(t){var e=["__\0is_clz",Mm++,Math.random().toFixed(3)].join("_");t.prototype[e]=!0,t.isInstance=function(t){return!(!t||!t[e])}}function Rn(t,e){var n=A(arguments,2);return this.superClass.prototype[e].apply(t,n)}function Bn(t,e,n){return this.superClass.prototype[e].apply(t,n)}function Vn(t,e){function n(t){var e=i[t.main];return e&&e[bm]||((e=i[t.main]={})[bm]=!0),e}e=e||{};var i={};if(t.registerClass=function(t,e){return e&&(zn(e),(e=On(e)).sub?e.sub!==bm&&(n(e)[e.sub]=t):i[e.main]=t),t},t.getClass=function(t,e,n){var r=i[t];if(r&&r[bm]&&(r=e?r[e]:null),n&&!r)throw new Error(e?"Component "+t+"."+(e||"")+" not exists. Load it first.":t+".type should be specified.");return r},t.getClassesByMainType=function(t){t=On(t);var e=[],n=i[t.main];return n&&n[bm]?d(n,function(t,n){n!==bm&&e.push(t)}):e.push(n),e},t.hasClass=function(t){return t=On(t),!!i[t.main]},t.getAllClassMainTypes=function(){var t=[];return d(i,function(e,n){t.push(n)}),t},t.hasSubTypes=function(t){t=On(t);var e=i[t.main];return e&&e[bm]},t.parseClassType=On,e.registerWhenExtend){var r=t.extend;r&&(t.extend=function(e){var n=r.call(this,e);return t.registerClass(n,e.type)})}return t}function Fn(t){return t>-Pm&&t<Pm}function Hn(t){return t>Pm||t<-Pm}function Gn(t,e,n,i,r){var o=1-r;return o*o*(o*t+3*r*e)+r*r*(r*i+3*o*n)}function Wn(t,e,n,i,r){var o=1-r;return 3*(((e-t)*o+2*(n-e)*r)*o+(i-n)*r*r)}function Zn(t,e,n,i,r,o){var a=i+3*(e-n)-t,s=3*(n-2*e+t),l=3*(e-t),u=t-r,h=s*s-3*a*l,c=s*l-9*a*u,d=l*l-3*s*u,f=0;if(Fn(h)&&Fn(c))Fn(s)?o[0]=0:(S=-l/s)>=0&&S<=1&&(o[f++]=S);else{var p=c*c-4*h*d;if(Fn(p)){var g=c/h,m=-g/2;(S=-s/a+g)>=0&&S<=1&&(o[f++]=S),m>=0&&m<=1&&(o[f++]=m)}else if(p>0){var v=km(p),y=h*s+1.5*a*(-c+v),x=h*s+1.5*a*(-c-v);(S=(-s-((y=y<0?-Am(-y,zm):Am(y,zm))+(x=x<0?-Am(-x,zm):Am(x,zm))))/(3*a))>=0&&S<=1&&(o[f++]=S)}else{var _=(2*h*s-3*a*c)/(2*km(h*h*h)),w=Math.acos(_)/3,b=km(h),M=Math.cos(w),S=(-s-2*b*M)/(3*a),m=(-s+b*(M+Om*Math.sin(w)))/(3*a),I=(-s+b*(M-Om*Math.sin(w)))/(3*a);S>=0&&S<=1&&(o[f++]=S),m>=0&&m<=1&&(o[f++]=m),I>=0&&I<=1&&(o[f++]=I)}}return f}function Un(t,e,n,i,r){var o=6*n-12*e+6*t,a=9*e+3*i-3*t-9*n,s=3*e-3*t,l=0;if(Fn(a))Hn(o)&&(c=-s/o)>=0&&c<=1&&(r[l++]=c);else{var u=o*o-4*a*s;if(Fn(u))r[0]=-o/(2*a);else if(u>0){var h=km(u),c=(-o+h)/(2*a),d=(-o-h)/(2*a);c>=0&&c<=1&&(r[l++]=c),d>=0&&d<=1&&(r[l++]=d)}}return l}function Xn(t,e,n,i,r,o){var a=(e-t)*r+t,s=(n-e)*r+e,l=(i-n)*r+n,u=(s-a)*r+a,h=(l-s)*r+s,c=(h-u)*r+u;o[0]=t,o[1]=a,o[2]=u,o[3]=c,o[4]=c,o[5]=h,o[6]=l,o[7]=i}function jn(t,e,n,i,r,o,a,s,l,u,h){var c,d,f,p,g,m=.005,v=1/0;Em[0]=l,Em[1]=u;for(var y=0;y<1;y+=.05)Nm[0]=Gn(t,n,r,a,y),Nm[1]=Gn(e,i,o,s,y),(p=Hp(Em,Nm))<v&&(c=y,v=p);v=1/0;for(var x=0;x<32&&!(m<Lm);x++)d=c-m,f=c+m,Nm[0]=Gn(t,n,r,a,d),Nm[1]=Gn(e,i,o,s,d),p=Hp(Nm,Em),d>=0&&p<v?(c=d,v=p):(Rm[0]=Gn(t,n,r,a,f),Rm[1]=Gn(e,i,o,s,f),g=Hp(Rm,Em),f<=1&&g<v?(c=f,v=g):m*=.5);return h&&(h[0]=Gn(t,n,r,a,c),h[1]=Gn(e,i,o,s,c)),km(v)}function Yn(t,e,n,i){var r=1-i;return r*(r*t+2*i*e)+i*i*n}function qn(t,e,n,i){return 2*((1-i)*(e-t)+i*(n-e))}function $n(t,e,n,i,r){var o=t-2*e+n,a=2*(e-t),s=t-i,l=0;if(Fn(o))Hn(a)&&(c=-s/a)>=0&&c<=1&&(r[l++]=c);else{var u=a*a-4*o*s;if(Fn(u))(c=-a/(2*o))>=0&&c<=1&&(r[l++]=c);else if(u>0){var h=km(u),c=(-a+h)/(2*o),d=(-a-h)/(2*o);c>=0&&c<=1&&(r[l++]=c),d>=0&&d<=1&&(r[l++]=d)}}return l}function Kn(t,e,n){var i=t+n-2*e;return 0===i?.5:(t-e)/i}function Qn(t,e,n,i,r){var o=(e-t)*i+t,a=(n-e)*i+e,s=(a-o)*i+o;r[0]=t,r[1]=o,r[2]=s,r[3]=s,r[4]=a,r[5]=n}function Jn(t,e,n,i,r,o,a,s,l){var u,h=.005,c=1/0;Em[0]=a,Em[1]=s;for(var d=0;d<1;d+=.05)Nm[0]=Yn(t,n,r,d),Nm[1]=Yn(e,i,o,d),(m=Hp(Em,Nm))<c&&(u=d,c=m);c=1/0;for(var f=0;f<32&&!(h<Lm);f++){var p=u-h,g=u+h;Nm[0]=Yn(t,n,r,p),Nm[1]=Yn(e,i,o,p);var m=Hp(Nm,Em);if(p>=0&&m<c)u=p,c=m;else{Rm[0]=Yn(t,n,r,g),Rm[1]=Yn(e,i,o,g);var v=Hp(Rm,Em);g<=1&&v<c?(u=g,c=v):h*=.5}}return l&&(l[0]=Yn(t,n,r,u),l[1]=Yn(e,i,o,u)),km(c)}function ti(t,e,n){if(0!==t.length){var i,r=t[0],o=r[0],a=r[0],s=r[1],l=r[1];for(i=1;i<t.length;i++)r=t[i],o=Bm(o,r[0]),a=Vm(a,r[0]),s=Bm(s,r[1]),l=Vm(l,r[1]);e[0]=o,e[1]=s,n[0]=a,n[1]=l}}function ei(t,e,n,i,r,o){r[0]=Bm(t,n),r[1]=Bm(e,i),o[0]=Vm(t,n),o[1]=Vm(e,i)}function ni(t,e,n,i,r,o,a,s,l,u){var h,c=Un,d=Gn,f=c(t,n,r,a,Xm);for(l[0]=1/0,l[1]=1/0,u[0]=-1/0,u[1]=-1/0,h=0;h<f;h++){var p=d(t,n,r,a,Xm[h]);l[0]=Bm(p,l[0]),u[0]=Vm(p,u[0])}for(f=c(e,i,o,s,jm),h=0;h<f;h++){var g=d(e,i,o,s,jm[h]);l[1]=Bm(g,l[1]),u[1]=Vm(g,u[1])}l[0]=Bm(t,l[0]),u[0]=Vm(t,u[0]),l[0]=Bm(a,l[0]),u[0]=Vm(a,u[0]),l[1]=Bm(e,l[1]),u[1]=Vm(e,u[1]),l[1]=Bm(s,l[1]),u[1]=Vm(s,u[1])}function ii(t,e,n,i,r,o,a,s){var l=Kn,u=Yn,h=Vm(Bm(l(t,n,r),1),0),c=Vm(Bm(l(e,i,o),1),0),d=u(t,n,r,h),f=u(e,i,o,c);a[0]=Bm(t,r,d),a[1]=Bm(e,o,f),s[0]=Vm(t,r,d),s[1]=Vm(e,o,f)}function ri(t,e,n,i,r,o,a,s,l){var u=K,h=Q,c=Math.abs(r-o);if(c%Gm<1e-4&&c>1e-4)return s[0]=t-n,s[1]=e-i,l[0]=t+n,void(l[1]=e+i);if(Wm[0]=Hm(r)*n+t,Wm[1]=Fm(r)*i+e,Zm[0]=Hm(o)*n+t,Zm[1]=Fm(o)*i+e,u(s,Wm,Zm),h(l,Wm,Zm),(r%=Gm)<0&&(r+=Gm),(o%=Gm)<0&&(o+=Gm),r>o&&!a?o+=Gm:r<o&&a&&(r+=Gm),a){var d=o;o=r,r=d}for(var f=0;f<o;f+=Math.PI/2)f>r&&(Um[0]=Hm(f)*n+t,Um[1]=Fm(f)*i+e,u(s,Um,s),h(l,Um,l))}function oi(t,e,n,i,r,o,a){if(0===r)return!1;var s=r,l=0,u=t;if(a>e+s&&a>i+s||a<e-s&&a<i-s||o>t+s&&o>n+s||o<t-s&&o<n-s)return!1;if(t===n)return Math.abs(o-t)<=s/2;var h=(l=(e-i)/(t-n))*o-a+(u=(t*i-n*e)/(t-n));return h*h/(l*l+1)<=s/2*s/2}function ai(t,e,n,i,r,o,a,s,l,u,h){if(0===l)return!1;var c=l;return!(h>e+c&&h>i+c&&h>o+c&&h>s+c||h<e-c&&h<i-c&&h<o-c&&h<s-c||u>t+c&&u>n+c&&u>r+c&&u>a+c||u<t-c&&u<n-c&&u<r-c&&u<a-c)&&jn(t,e,n,i,r,o,a,s,u,h,null)<=c/2}function si(t,e,n,i,r,o,a,s,l){if(0===a)return!1;var u=a;return!(l>e+u&&l>i+u&&l>o+u||l<e-u&&l<i-u&&l<o-u||s>t+u&&s>n+u&&s>r+u||s<t-u&&s<n-u&&s<r-u)&&Jn(t,e,n,i,r,o,s,l,null)<=u/2}function li(t){return(t%=sv)<0&&(t+=sv),t}function ui(t,e,n,i,r,o,a,s,l){if(0===a)return!1;var u=a;s-=t,l-=e;var h=Math.sqrt(s*s+l*l);if(h-u>n||h+u<n)return!1;if(Math.abs(i-r)%lv<1e-4)return!0;if(o){var c=i;i=li(r),r=li(c)}else i=li(i),r=li(r);i>r&&(r+=lv);var d=Math.atan2(l,s);return d<0&&(d+=lv),d>=i&&d<=r||d+lv>=i&&d+lv<=r}function hi(t,e,n,i,r,o){if(o>e&&o>i||o<e&&o<i)return 0;if(i===e)return 0;var a=i<e?1:-1,s=(o-e)/(i-e);1!==s&&0!==s||(a=i<e?.5:-.5);var l=s*(n-t)+t;return l===r?1/0:l>r?a:0}function ci(t,e){return Math.abs(t-e)<cv}function di(){var t=fv[0];fv[0]=fv[1],fv[1]=t}function fi(t,e,n,i,r,o,a,s,l,u){if(u>e&&u>i&&u>o&&u>s||u<e&&u<i&&u<o&&u<s)return 0;var h=Zn(e,i,o,s,u,dv);if(0===h)return 0;for(var c,d,f=0,p=-1,g=0;g<h;g++){var m=dv[g],v=0===m||1===m?.5:1;Gn(t,n,r,a,m)<l||(p<0&&(p=Un(e,i,o,s,fv),fv[1]<fv[0]&&p>1&&di(),c=Gn(e,i,o,s,fv[0]),p>1&&(d=Gn(e,i,o,s,fv[1]))),2==p?m<fv[0]?f+=c<e?v:-v:m<fv[1]?f+=d<c?v:-v:f+=s<d?v:-v:m<fv[0]?f+=c<e?v:-v:f+=s<c?v:-v)}return f}function pi(t,e,n,i,r,o,a,s){if(s>e&&s>i&&s>o||s<e&&s<i&&s<o)return 0;var l=$n(e,i,o,s,dv);if(0===l)return 0;var u=Kn(e,i,o);if(u>=0&&u<=1){for(var h=0,c=Yn(e,i,o,u),d=0;d<l;d++){f=0===dv[d]||1===dv[d]?.5:1;(p=Yn(t,n,r,dv[d]))<a||(dv[d]<u?h+=c<e?f:-f:h+=o<c?f:-f)}return h}var f=0===dv[0]||1===dv[0]?.5:1,p=Yn(t,n,r,dv[0]);return p<a?0:o<e?f:-f}function gi(t,e,n,i,r,o,a,s){if((s-=e)>n||s<-n)return 0;u=Math.sqrt(n*n-s*s);dv[0]=-u,dv[1]=u;var l=Math.abs(i-r);if(l<1e-4)return 0;if(l%hv<1e-4){i=0,r=hv;p=o?1:-1;return a>=dv[0]+t&&a<=dv[1]+t?p:0}if(o){var u=i;i=li(r),r=li(u)}else i=li(i),r=li(r);i>r&&(r+=hv);for(var h=0,c=0;c<2;c++){var d=dv[c];if(d+t>a){var f=Math.atan2(s,d),p=o?1:-1;f<0&&(f=hv+f),(f>=i&&f<=r||f+hv>=i&&f+hv<=r)&&(f>Math.PI/2&&f<1.5*Math.PI&&(p=-p),h+=p)}}return h}function mi(t,e,n,i,r){for(var o=0,a=0,s=0,l=0,u=0,h=0;h<t.length;){var c=t[h++];switch(c===uv.M&&h>1&&(n||(o+=hi(a,s,l,u,i,r))),1==h&&(l=a=t[h],u=s=t[h+1]),c){case uv.M:a=l=t[h++],s=u=t[h++];break;case uv.L:if(n){if(oi(a,s,t[h],t[h+1],e,i,r))return!0}else o+=hi(a,s,t[h],t[h+1],i,r)||0;a=t[h++],s=t[h++];break;case uv.C:if(n){if(ai(a,s,t[h++],t[h++],t[h++],t[h++],t[h],t[h+1],e,i,r))return!0}else o+=fi(a,s,t[h++],t[h++],t[h++],t[h++],t[h],t[h+1],i,r)||0;a=t[h++],s=t[h++];break;case uv.Q:if(n){if(si(a,s,t[h++],t[h++],t[h],t[h+1],e,i,r))return!0}else o+=pi(a,s,t[h++],t[h++],t[h],t[h+1],i,r)||0;a=t[h++],s=t[h++];break;case uv.A:var d=t[h++],f=t[h++],p=t[h++],g=t[h++],m=t[h++],v=t[h++],y=(t[h++],1-t[h++]),x=Math.cos(m)*p+d,_=Math.sin(m)*g+f;h>1?o+=hi(a,s,x,_,i,r):(l=x,u=_);var w=(i-d)*g/p+d;if(n){if(ui(d,f,g,m,m+v,y,e,w,r))return!0}else o+=gi(d,f,g,m,m+v,y,w,r);a=Math.cos(m+v)*p+d,s=Math.sin(m+v)*g+f;break;case uv.R:l=a=t[h++],u=s=t[h++];var x=l+t[h++],_=u+t[h++];if(n){if(oi(l,u,x,u,e,i,r)||oi(x,u,x,_,e,i,r)||oi(x,_,l,_,e,i,r)||oi(l,_,l,u,e,i,r))return!0}else o+=hi(x,u,x,_,i,r),o+=hi(l,_,l,u,i,r);break;case uv.Z:if(n){if(oi(a,s,l,u,e,i,r))return!0}else o+=hi(a,s,l,u,i,r);a=l,s=u}}return n||ci(s,u)||(o+=hi(a,s,l,u,i,r)||0),0!==o}function vi(t,e,n){return mi(t,0,!1,e,n)}function yi(t,e,n,i){return mi(t,e,!0,n,i)}function xi(t){Xe.call(this,t),this.path=null}function _i(t,e,n,i,r,o,a,s,l,u,h){var c=l*(Cv/180),d=Iv(c)*(t-n)/2+Sv(c)*(e-i)/2,f=-1*Sv(c)*(t-n)/2+Iv(c)*(e-i)/2,p=d*d/(a*a)+f*f/(s*s);p>1&&(a*=Mv(p),s*=Mv(p));var g=(r===o?-1:1)*Mv((a*a*(s*s)-a*a*(f*f)-s*s*(d*d))/(a*a*(f*f)+s*s*(d*d)))||0,m=g*a*f/s,v=g*-s*d/a,y=(t+n)/2+Iv(c)*m-Sv(c)*v,x=(e+i)/2+Sv(c)*m+Iv(c)*v,_=Av([1,0],[(d-m)/a,(f-v)/s]),w=[(d-m)/a,(f-v)/s],b=[(-1*d-m)/a,(-1*f-v)/s],M=Av(w,b);Dv(w,b)<=-1&&(M=Cv),Dv(w,b)>=1&&(M=0),0===o&&M>0&&(M-=2*Cv),1===o&&M<0&&(M+=2*Cv),h.addData(u,y,x,a,s,_,M,c,o)}function wi(t){if(!t)return[];var e,n=t.replace(/-/g," -").replace(/ /g," ").replace(/ /g,",").replace(/,,/g,",");for(e=0;e<bv.length;e++)n=n.replace(new RegExp(bv[e],"g"),"|"+bv[e]);var i,r=n.split("|"),o=0,a=0,s=new av,l=av.CMD;for(e=1;e<r.length;e++){var u,h=r[e],c=h.charAt(0),d=0,f=h.slice(1).replace(/e,-/g,"e-").split(",");f.length>0&&""===f[0]&&f.shift();for(var p=0;p<f.length;p++)f[p]=parseFloat(f[p]);for(;d<f.length&&!isNaN(f[d])&&!isNaN(f[0]);){var g,m,v,y,x,_,w,b=o,M=a;switch(c){case"l":o+=f[d++],a+=f[d++],u=l.L,s.addData(u,o,a);break;case"L":o=f[d++],a=f[d++],u=l.L,s.addData(u,o,a);break;case"m":o+=f[d++],a+=f[d++],u=l.M,s.addData(u,o,a),c="l";break;case"M":o=f[d++],a=f[d++],u=l.M,s.addData(u,o,a),c="L";break;case"h":o+=f[d++],u=l.L,s.addData(u,o,a);break;case"H":o=f[d++],u=l.L,s.addData(u,o,a);break;case"v":a+=f[d++],u=l.L,s.addData(u,o,a);break;case"V":a=f[d++],u=l.L,s.addData(u,o,a);break;case"C":u=l.C,s.addData(u,f[d++],f[d++],f[d++],f[d++],f[d++],f[d++]),o=f[d-2],a=f[d-1];break;case"c":u=l.C,s.addData(u,f[d++]+o,f[d++]+a,f[d++]+o,f[d++]+a,f[d++]+o,f[d++]+a),o+=f[d-2],a+=f[d-1];break;case"S":g=o,m=a;var S=s.len(),I=s.data;i===l.C&&(g+=o-I[S-4],m+=a-I[S-3]),u=l.C,b=f[d++],M=f[d++],o=f[d++],a=f[d++],s.addData(u,g,m,b,M,o,a);break;case"s":g=o,m=a;var S=s.len(),I=s.data;i===l.C&&(g+=o-I[S-4],m+=a-I[S-3]),u=l.C,b=o+f[d++],M=a+f[d++],o+=f[d++],a+=f[d++],s.addData(u,g,m,b,M,o,a);break;case"Q":b=f[d++],M=f[d++],o=f[d++],a=f[d++],u=l.Q,s.addData(u,b,M,o,a);break;case"q":b=f[d++]+o,M=f[d++]+a,o+=f[d++],a+=f[d++],u=l.Q,s.addData(u,b,M,o,a);break;case"T":g=o,m=a;var S=s.len(),I=s.data;i===l.Q&&(g+=o-I[S-4],m+=a-I[S-3]),o=f[d++],a=f[d++],u=l.Q,s.addData(u,g,m,o,a);break;case"t":g=o,m=a;var S=s.len(),I=s.data;i===l.Q&&(g+=o-I[S-4],m+=a-I[S-3]),o+=f[d++],a+=f[d++],u=l.Q,s.addData(u,g,m,o,a);break;case"A":v=f[d++],y=f[d++],x=f[d++],_=f[d++],w=f[d++],_i(b=o,M=a,o=f[d++],a=f[d++],_,w,v,y,x,u=l.A,s);break;case"a":v=f[d++],y=f[d++],x=f[d++],_=f[d++],w=f[d++],_i(b=o,M=a,o+=f[d++],a+=f[d++],_,w,v,y,x,u=l.A,s)}}"z"!==c&&"Z"!==c||(u=l.Z,s.addData(u)),i=u}return s.toStatic(),s}function bi(t,e){var n=wi(t);return e=e||{},e.buildPath=function(t){if(t.setData)t.setData(n.data),(e=t.getContext())&&t.rebuildPath(e);else{var e=t;n.rebuildPath(e)}},e.applyTransform=function(t){wv(n,t),this.dirty(!0)},e}function Mi(t,e){return new xi(bi(t,e))}function Si(t,e){return xi.extend(bi(t,e))}function Ii(t,e,n,i,r,o,a){var s=.5*(n-t),l=.5*(i-e);return(2*(e-n)+s+l)*a+(-3*(e-n)-2*s-l)*o+s*r+e}function Ci(t,e,n){var i=e.points,r=e.smooth;if(i&&i.length>=2){if(r&&"spline"!==r){var o=Rv(i,r,n,e.smoothConstraint);t.moveTo(i[0][0],i[0][1]);for(var a=i.length,s=0;s<(n?a:a-1);s++){var l=o[2*s],u=o[2*s+1],h=i[(s+1)%a];t.bezierCurveTo(l[0],l[1],u[0],u[1],h[0],h[1])}}else{"spline"===r&&(i=Nv(i,n)),t.moveTo(i[0][0],i[0][1]);for(var s=1,c=i.length;s<c;s++)t.lineTo(i[s][0],i[s][1])}n&&t.closePath()}}function Ti(t,e,n){var i=t.cpx2,r=t.cpy2;return null===i||null===r?[(n?Wn:Gn)(t.x1,t.cpx1,t.cpx2,t.x2,e),(n?Wn:Gn)(t.y1,t.cpy1,t.cpy2,t.y2,e)]:[(n?qn:Yn)(t.x1,t.cpx1,t.x2,e),(n?qn:Yn)(t.y1,t.cpy1,t.y2,e)]}function Di(t){Xe.call(this,t),this._displayables=[],this._temporaryDisplayables=[],this._cursor=0,this.notClear=!0}function Ai(t){return xi.extend(t)}function ki(t,e,n,i){var r=Mi(t,e),o=r.getBoundingRect();return n&&("center"===i&&(n=Li(n,o)),Oi(r,n)),r}function Pi(t,e,n){var i=new je({style:{image:t,x:e.x,y:e.y,width:e.width,height:e.height},onload:function(t){if("center"===n){var r={width:t.width,height:t.height};i.setStyle(Li(e,r))}}});return i}function Li(t,e){var n,i=e.width/e.height,r=t.height*i;return n=r<=t.width?t.height:(r=t.width)/i,{x:t.x+t.width/2-r/2,y:t.y+t.height/2-n/2,width:r,height:n}}function Oi(t,e){if(t.applyTransform){var n=t.getBoundingRect().calculateTransform(e);t.applyTransform(n)}}function zi(t){var e=t.shape,n=t.style.lineWidth;return $v(2*e.x1)===$v(2*e.x2)&&(e.x1=e.x2=Ni(e.x1,n,!0)),$v(2*e.y1)===$v(2*e.y2)&&(e.y1=e.y2=Ni(e.y1,n,!0)),t}function Ei(t){var e=t.shape,n=t.style.lineWidth,i=e.x,r=e.y,o=e.width,a=e.height;return e.x=Ni(e.x,n,!0),e.y=Ni(e.y,n,!0),e.width=Math.max(Ni(i+o,n,!1)-e.x,0===o?0:1),e.height=Math.max(Ni(r+a,n,!1)-e.y,0===a?0:1),t}function Ni(t,e,n){var i=$v(2*t);return(i+$v(e))%2==0?i/2:(i+(n?1:-1))/2}function Ri(t){return null!=t&&"none"!=t}function Bi(t){return"string"==typeof t?Tt(t,-.1):t}function Vi(t){if(t.__hoverStlDirty){var e=t.style.stroke,n=t.style.fill,i=t.__hoverStl;i.fill=i.fill||(Ri(n)?Bi(n):null),i.stroke=i.stroke||(Ri(e)?Bi(e):null);var r={};for(var o in i)null!=i[o]&&(r[o]=t.style[o]);t.__normalStl=r,t.__hoverStlDirty=!1}}function Fi(t){if(!t.__isHover){if(Vi(t),t.useHoverLayer)t.__zr&&t.__zr.addHover(t,t.__hoverStl);else{var e=t.style,n=e.insideRollbackOpt;n&&ir(e),e.extendFrom(t.__hoverStl),n&&(nr(e,e.insideOriginalTextPosition,n),null==e.textFill&&(e.textFill=n.autoColor)),t.dirty(!1),t.z2+=1}t.__isHover=!0}}function Hi(t){if(t.__isHover){var e=t.__normalStl;t.useHoverLayer?t.__zr&&t.__zr.removeHover(t):(e&&t.setStyle(e),t.z2-=1),t.__isHover=!1}}function Gi(t){"group"===t.type?t.traverse(function(t){"group"!==t.type&&Fi(t)}):Fi(t)}function Wi(t){"group"===t.type?t.traverse(function(t){"group"!==t.type&&Hi(t)}):Hi(t)}function Zi(t,e){t.__hoverStl=t.hoverStyle||e||{},t.__hoverStlDirty=!0,t.__isHover&&Vi(t)}function Ui(t){this.__hoverSilentOnTouch&&t.zrByTouch||!this.__isEmphasis&&Gi(this)}function Xi(t){this.__hoverSilentOnTouch&&t.zrByTouch||!this.__isEmphasis&&Wi(this)}function ji(){this.__isEmphasis=!0,Gi(this)}function Yi(){this.__isEmphasis=!1,Wi(this)}function qi(t,e,n){t.__hoverSilentOnTouch=n&&n.hoverSilentOnTouch,"group"===t.type?t.traverse(function(t){"group"!==t.type&&Zi(t,e)}):Zi(t,e),t.on("mouseover",Ui).on("mouseout",Xi),t.on("emphasis",ji).on("normal",Yi)}function $i(t,e,n,i,r,o,a){var s,l=(r=r||Jv).labelFetcher,u=r.labelDataIndex,h=r.labelDimIndex,c=n.getShallow("show"),d=i.getShallow("show");(c||d)&&(l&&(s=l.getFormattedLabel(u,"normal",null,h)),null==s&&(s=x(r.defaultText)?r.defaultText(u,r):r.defaultText));var f=c?s:null,p=d?T(l?l.getFormattedLabel(u,"emphasis",null,h):null,s):null;null==f&&null==p||(Ki(t,n,o,r),Ki(e,i,a,r,!0)),t.text=f,e.text=p}function Ki(t,e,n,i,r){return Qi(t,e,i,r),n&&o(t,n),t.host&&t.host.dirty&&t.host.dirty(!1),t}function Qi(t,e,n,i){if((n=n||Jv).isRectText){var r=e.getShallow("position")||(i?null:"inside");"outside"===r&&(r="top"),t.textPosition=r,t.textOffset=e.getShallow("offset");var o=e.getShallow("rotate");null!=o&&(o*=Math.PI/180),t.textRotation=o,t.textDistance=T(e.getShallow("distance"),i?null:5)}var a,s=e.ecModel,l=s&&s.option.textStyle,u=Ji(e);if(u){a={};for(var h in u)if(u.hasOwnProperty(h)){var c=e.getModel(["rich",h]);tr(a[h]={},c,l,n,i)}}return t.rich=a,tr(t,e,l,n,i,!0),n.forceRich&&!n.textStyle&&(n.textStyle={}),t}function Ji(t){for(var e;t&&t!==t.ecModel;){var n=(t.option||Jv).rich;if(n){e=e||{};for(var i in n)n.hasOwnProperty(i)&&(e[i]=1)}t=t.parentModel}return e}function tr(t,e,n,i,r,o){if(n=!r&&n||Jv,t.textFill=er(e.getShallow("color"),i)||n.color,t.textStroke=er(e.getShallow("textBorderColor"),i)||n.textBorderColor,t.textStrokeWidth=T(e.getShallow("textBorderWidth"),n.textBorderWidth),!r){if(o){var a=t.textPosition;t.insideRollback=nr(t,a,i),t.insideOriginalTextPosition=a,t.insideRollbackOpt=i}null==t.textFill&&(t.textFill=i.autoColor)}t.fontStyle=e.getShallow("fontStyle")||n.fontStyle,t.fontWeight=e.getShallow("fontWeight")||n.fontWeight,t.fontSize=e.getShallow("fontSize")||n.fontSize,t.fontFamily=e.getShallow("fontFamily")||n.fontFamily,t.textAlign=e.getShallow("align"),t.textVerticalAlign=e.getShallow("verticalAlign")||e.getShallow("baseline"),t.textLineHeight=e.getShallow("lineHeight"),t.textWidth=e.getShallow("width"),t.textHeight=e.getShallow("height"),t.textTag=e.getShallow("tag"),o&&i.disableBox||(t.textBackgroundColor=er(e.getShallow("backgroundColor"),i),t.textPadding=e.getShallow("padding"),t.textBorderColor=er(e.getShallow("borderColor"),i),t.textBorderWidth=e.getShallow("borderWidth"),t.textBorderRadius=e.getShallow("borderRadius"),t.textBoxShadowColor=e.getShallow("shadowColor"),t.textBoxShadowBlur=e.getShallow("shadowBlur"),t.textBoxShadowOffsetX=e.getShallow("shadowOffsetX"),t.textBoxShadowOffsetY=e.getShallow("shadowOffsetY")),t.textShadowColor=e.getShallow("textShadowColor")||n.textShadowColor,t.textShadowBlur=e.getShallow("textShadowBlur")||n.textShadowBlur,t.textShadowOffsetX=e.getShallow("textShadowOffsetX")||n.textShadowOffsetX,t.textShadowOffsetY=e.getShallow("textShadowOffsetY")||n.textShadowOffsetY}function er(t,e){return"auto"!==t?t:e&&e.autoColor?e.autoColor:null}function nr(t,e,n){var i,r=n.useInsideStyle;return null==t.textFill&&!1!==r&&(!0===r||n.isRectText&&e&&"string"==typeof e&&e.indexOf("inside")>=0)&&(i={textFill:null,textStroke:t.textStroke,textStrokeWidth:t.textStrokeWidth},t.textFill="#fff",null==t.textStroke&&(t.textStroke=n.autoColor,null==t.textStrokeWidth&&(t.textStrokeWidth=2))),i}function ir(t){var e=t.insideRollback;e&&(t.textFill=e.textFill,t.textStroke=e.textStroke,t.textStrokeWidth=e.textStrokeWidth)}function rr(t,e){var n=e||e.getModel("textStyle");return L([t.fontStyle||n&&n.getShallow("fontStyle")||"",t.fontWeight||n&&n.getShallow("fontWeight")||"",(t.fontSize||n&&n.getShallow("fontSize")||12)+"px",t.fontFamily||n&&n.getShallow("fontFamily")||"sans-serif"].join(" "))}function or(t,e,n,i,r,o){if("function"==typeof r&&(o=r,r=null),i&&i.isAnimationEnabled()){var a=t?"Update":"",s=i.getShallow("animationDuration"+a),l=i.getShallow("animationEasing"+a),u=i.getShallow("animationDelay"+a);"function"==typeof u&&(u=u(r,i.getAnimationDelayParams?i.getAnimationDelayParams(e,r):null)),"function"==typeof s&&(s=s(r)),s>0?e.animateTo(n,s,u||0,l,o,!!o):(e.stopAnimation(),e.attr(n),o&&o())}else e.stopAnimation(),e.attr(n),o&&o()}function ar(t,e,n,i,r){or(!0,t,e,n,i,r)}function sr(t,e,n,i,r){or(!1,t,e,n,i,r)}function lr(t,e){for(var n=ot([]);t&&t!==e;)st(n,t.getLocalTransform(),n),t=t.parent;return n}function ur(t,e,n){return e&&!c(e)&&(e=Qp.getLocalTransform(e)),n&&(e=ct([],e)),$([],t,e)}function hr(t,e,n){var i=0===e[4]||0===e[5]||0===e[0]?1:Math.abs(2*e[4]/e[0]),r=0===e[4]||0===e[5]||0===e[2]?1:Math.abs(2*e[4]/e[2]),o=["left"===t?-i:"right"===t?i:0,"top"===t?-r:"bottom"===t?r:0];return o=ur(o,e,n),Math.abs(o[0])>Math.abs(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function cr(t,e,n,i){function r(t){var e={position:F(t.position),rotation:t.rotation};return t.shape&&(e.shape=o({},t.shape)),e}if(t&&e){var a=function(t){var e={};return t.traverse(function(t){!t.isGroup&&t.anid&&(e[t.anid]=t)}),e}(t);e.traverse(function(t){if(!t.isGroup&&t.anid){var e=a[t.anid];if(e){var i=r(t);t.attr(r(e)),ar(t,i,n,t.dataIndex)}}})}}function dr(t,e){return f(t,function(t){var n=t[0];n=Kv(n,e.x),n=Qv(n,e.x+e.width);var i=t[1];return i=Kv(i,e.y),i=Qv(i,e.y+e.height),[n,i]})}function fr(t,e,n){var i=(e=o({rectHover:!0},e)).style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},t)return 0===t.indexOf("image://")?(i.image=t.slice(8),a(i,n),new je(e)):ki(t.replace("path://",""),e,n,"center")}function pr(t,e,n){this.parentModel=e,this.ecModel=n,this.option=t}function gr(t,e,n){for(var i=0;i<e.length&&(!e[i]||null!=(t=t&&"object"==typeof t?t[e[i]]:null));i++);return null==t&&n&&(t=n.get(e)),t}function mr(t,e){var n=ay(t).getParent;return n?n.call(t,e):t.parentModel}function vr(t){return[t||"",sy++,Math.random().toFixed(5)].join("_")}function yr(t){return t.replace(/^\s+/,"").replace(/\s+$/,"")}function xr(t,e,n,i){var r=e[1]-e[0],o=n[1]-n[0];if(0===r)return 0===o?n[0]:(n[0]+n[1])/2;if(i)if(r>0){if(t<=e[0])return n[0];if(t>=e[1])return n[1]}else{if(t>=e[0])return n[0];if(t<=e[1])return n[1]}else{if(t===e[0])return n[0];if(t===e[1])return n[1]}return(t-e[0])/r*o+n[0]}function _r(t,e){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return"string"==typeof t?yr(t).match(/%$/)?parseFloat(t)/100*e:parseFloat(t):null==t?NaN:+t}function wr(t,e,n){return null==e&&(e=10),e=Math.min(Math.max(0,e),20),t=(+t).toFixed(e),n?t:+t}function br(t){return t.sort(function(t,e){return t-e}),t}function Mr(t){if(t=+t,isNaN(t))return 0;for(var e=1,n=0;Math.round(t*e)/e!==t;)e*=10,n++;return n}function Sr(t){var e=t.toString(),n=e.indexOf("e");if(n>0){var i=+e.slice(n+1);return i<0?-i:0}var r=e.indexOf(".");return r<0?0:e.length-1-r}function Ir(t,e){var n=Math.log,i=Math.LN10,r=Math.floor(n(t[1]-t[0])/i),o=Math.round(n(Math.abs(e[1]-e[0]))/i),a=Math.min(Math.max(-r+o,0),20);return isFinite(a)?a:20}function Cr(t,e,n){if(!t[e])return 0;var i=p(t,function(t,e){return t+(isNaN(e)?0:e)},0);if(0===i)return 0;for(var r=Math.pow(10,n),o=f(t,function(t){return(isNaN(t)?0:t)/i*r*100}),a=100*r,s=f(o,function(t){return Math.floor(t)}),l=p(s,function(t,e){return t+e},0),u=f(o,function(t,e){return t-s[e]});l<a;){for(var h=Number.NEGATIVE_INFINITY,c=null,d=0,g=u.length;d<g;++d)u[d]>h&&(h=u[d],c=d);++s[c],u[c]=0,++l}return s[e]/r}function Tr(t){var e=2*Math.PI;return(t%e+e)%e}function Dr(t){return t>-ly&&t<ly}function Ar(t){if(t instanceof Date)return t;if("string"==typeof t){var e=uy.exec(t);if(!e)return new Date(NaN);if(e[8]){var n=+e[4]||0;return"Z"!==e[8].toUpperCase()&&(n-=e[8].slice(0,3)),new Date(Date.UTC(+e[1],+(e[2]||1)-1,+e[3]||1,n,+(e[5]||0),+e[6]||0,+e[7]||0))}return new Date(+e[1],+(e[2]||1)-1,+e[3]||1,+e[4]||0,+(e[5]||0),+e[6]||0,+e[7]||0)}return null==t?new Date(NaN):new Date(Math.round(t))}function kr(t){return Math.pow(10,Pr(t))}function Pr(t){return Math.floor(Math.log(t)/Math.LN10)}function Lr(t,e){var n,i=Pr(t),r=Math.pow(10,i),o=t/r;return n=e?o<1.5?1:o<2.5?2:o<4?3:o<7?5:10:o<1?1:o<2?2:o<3?3:o<5?5:10,t=n*r,i>=-20?+t.toFixed(i<0?-i:0):t}function Or(t){return isNaN(t)?"-":(t=(t+"").split("."))[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(t.length>1?"."+t[1]:"")}function zr(t,e){return t=(t||"").toLowerCase().replace(/-(.)/g,function(t,e){return e.toUpperCase()}),e&&t&&(t=t.charAt(0).toUpperCase()+t.slice(1)),t}function Er(t){return null==t?"":(t+"").replace(dy,function(t,e){return fy[e]})}function Nr(t,e,n){y(e)||(e=[e]);var i=e.length;if(!i)return"";for(var r=e[0].$vars||[],o=0;o<r.length;o++){var a=py[o];t=t.replace(gy(a),gy(a,0))}for(var s=0;s<i;s++)for(var l=0;l<r.length;l++){var u=e[s][r[l]];t=t.replace(gy(py[l],s),n?Er(u):u)}return t}function Rr(t,e){var n=(t=_(t)?{color:t,extraCssText:e}:t||{}).color,i=t.type,e=t.extraCssText;return n?"subItem"===i?'<span style="display:inline-block;vertical-align:middle;margin-right:8px;margin-left:3px;border-radius:4px;width:4px;height:4px;background-color:'+Er(n)+";"+(e||"")+'"></span>':'<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:'+Er(n)+";"+(e||"")+'"></span>':""}function Br(t,e){return t+="","0000".substr(0,e-t.length)+t}function Vr(t,e,n){"week"!==t&&"month"!==t&&"quarter"!==t&&"half-year"!==t&&"year"!==t||(t="MM-dd\nyyyy");var i=Ar(e),r=n?"UTC":"",o=i["get"+r+"FullYear"](),a=i["get"+r+"Month"]()+1,s=i["get"+r+"Date"](),l=i["get"+r+"Hours"](),u=i["get"+r+"Minutes"](),h=i["get"+r+"Seconds"](),c=i["get"+r+"Milliseconds"]();return t=t.replace("MM",Br(a,2)).replace("M",a).replace("yyyy",o).replace("yy",o%100).replace("dd",Br(s,2)).replace("d",s).replace("hh",Br(l,2)).replace("h",l).replace("mm",Br(u,2)).replace("m",u).replace("ss",Br(h,2)).replace("s",h).replace("SSS",Br(c,3))}function Fr(t){return t?t.charAt(0).toUpperCase()+t.substr(1):t}function Hr(t,e,n,i,r){var o=0,a=0;null==i&&(i=1/0),null==r&&(r=1/0);var s=0;e.eachChild(function(l,u){var h,c,d=l.position,f=l.getBoundingRect(),p=e.childAt(u+1),g=p&&p.getBoundingRect();if("horizontal"===t){var m=f.width+(g?-g.x+f.x:0);(h=o+m)>i||l.newline?(o=0,h=m,a+=s+n,s=f.height):s=Math.max(s,f.height)}else{var v=f.height+(g?-g.y+f.y:0);(c=a+v)>r||l.newline?(o+=s+n,a=0,c=v,s=f.width):s=Math.max(s,f.width)}l.newline||(d[0]=o,d[1]=a,"horizontal"===t?o=h+n:a=c+n)})}function Gr(t,e,n){n=cy(n||0);var i=e.width,r=e.height,o=_r(t.left,i),a=_r(t.top,r),s=_r(t.right,i),l=_r(t.bottom,r),u=_r(t.width,i),h=_r(t.height,r),c=n[2]+n[0],d=n[1]+n[3],f=t.aspect;switch(isNaN(u)&&(u=i-s-d-o),isNaN(h)&&(h=r-l-c-a),null!=f&&(isNaN(u)&&isNaN(h)&&(f>i/r?u=.8*i:h=.8*r),isNaN(u)&&(u=f*h),isNaN(h)&&(h=u/f)),isNaN(o)&&(o=i-s-u-d),isNaN(a)&&(a=r-l-h-c),t.left||t.right){case"center":o=i/2-u/2-n[3];break;case"right":o=i-u-d}switch(t.top||t.bottom){case"middle":case"center":a=r/2-h/2-n[0];break;case"bottom":a=r-h-c}o=o||0,a=a||0,isNaN(u)&&(u=i-d-o-(s||0)),isNaN(h)&&(h=r-c-a-(l||0));var p=new Xt(o+n[3],a+n[0],u,h);return p.margin=n,p}function Wr(t,e,n,i,r){var o=!r||!r.hv||r.hv[0],s=!r||!r.hv||r.hv[1],l=r&&r.boundingMode||"all";if(o||s){var u;if("raw"===l)u="group"===t.type?new Xt(0,0,+e.width||0,+e.height||0):t.getBoundingRect();else if(u=t.getBoundingRect(),t.needLocalTransform()){var h=t.getLocalTransform();(u=u.clone()).applyTransform(h)}e=Gr(a({width:u.width,height:u.height},e),n,i);var c=t.position,d=o?e.x-u.x:0,f=s?e.y-u.y:0;t.attr("position","raw"===l?[d,f]:[c[0]+d,c[1]+f])}}function Zr(t,e,n){function i(n,i){var a={},l=0,u={},h=0;if(xy(n,function(e){u[e]=t[e]}),xy(n,function(t){r(e,t)&&(a[t]=u[t]=e[t]),o(a,t)&&l++,o(u,t)&&h++}),s[i])return o(e,n[1])?u[n[2]]=null:o(e,n[2])&&(u[n[1]]=null),u;if(2!==h&&l){if(l>=2)return a;for(var c=0;c<n.length;c++){var d=n[c];if(!r(a,d)&&r(t,d)){a[d]=t[d];break}}return a}return u}function r(t,e){return t.hasOwnProperty(e)}function o(t,e){return null!=t[e]&&"auto"!==t[e]}function a(t,e,n){xy(t,function(t){e[t]=n[t]})}!w(n)&&(n={});var s=n.ignoreSize;!y(s)&&(s=[s,s]);var l=i(wy[0],0),u=i(wy[1],1);a(wy[0],t,l),a(wy[1],t,u)}function Ur(t){return Xr({},t)}function Xr(t,e){return e&&t&&xy(_y,function(n){e.hasOwnProperty(n)&&(t[n]=e[n])}),t}function jr(t,e){for(var n=t.length,i=0;i<n;i++)if(t[i].length>e)return t[i];return t[n-1]}function Yr(t){var e=t.get("coordinateSystem"),n={coordSysName:e,coordSysDims:[],axisMap:N(),categoryAxisMap:N()},i=ky[e];if(i)return i(t,n,n.axisMap,n.categoryAxisMap),n}function qr(t){return"category"===t.get("type")}function $r(t){this.fromDataset=t.fromDataset,this.data=t.data||(t.sourceFormat===zy?{}:[]),this.sourceFormat=t.sourceFormat||Ey,this.seriesLayoutBy=t.seriesLayoutBy||Ry,this.dimensionsDefine=t.dimensionsDefine,this.encodeDefine=t.encodeDefine&&N(t.encodeDefine),this.startIndex=t.startIndex||0,this.dimensionsDetectCount=t.dimensionsDetectCount}function Kr(t){var e=t.option.source,n=Ey;if(M(e))n=Ny;else if(y(e))for(var i=0,r=e.length;i<r;i++){var o=e[i];if(null!=o){if(y(o)){n=Ly;break}if(w(o)){n=Oy;break}}}else if(w(e)){for(var a in e)if(e.hasOwnProperty(a)&&c(e[a])){n=zy;break}}else if(null!=e)throw new Error("Invalid data");Vy(t).sourceFormat=n}function Qr(t){return Vy(t).source}function Jr(t){Vy(t).datasetMap=N()}function to(t){var e=t.option,n=e.data,i=M(n)?Ny:Py,r=!1,o=e.seriesLayoutBy,a=e.sourceHeader,s=e.dimensions,l=ao(t);if(l){var u=l.option;n=u.source,i=Vy(l).sourceFormat,r=!0,o=o||u.seriesLayoutBy,null==a&&(a=u.sourceHeader),s=s||u.dimensions}var h=eo(n,i,o,a,s),c=e.encode;!c&&l&&(c=oo(t,l,n,i,o,h)),Vy(t).source=new $r({data:n,fromDataset:r,seriesLayoutBy:o,sourceFormat:i,dimensionsDefine:h.dimensionsDefine,startIndex:h.startIndex,dimensionsDetectCount:h.dimensionsDetectCount,encodeDefine:c})}function eo(t,e,n,i,r){if(!t)return{dimensionsDefine:no(r)};var o,a,s;if(e===Ly)"auto"===i||null==i?io(function(t){null!=t&&"-"!==t&&(_(t)?null==a&&(a=1):a=0)},n,t,10):a=i?1:0,r||1!==a||(r=[],io(function(t,e){r[e]=null!=t?t:""},n,t)),o=r?r.length:n===By?t.length:t[0]?t[0].length:null;else if(e===Oy)r||(r=ro(t),s=!0);else if(e===zy)r||(r=[],s=!0,d(t,function(t,e){r.push(e)}));else if(e===Py){var l=wn(t[0]);o=y(l)&&l.length||1}var u;return s&&d(r,function(t,e){"name"===(w(t)?t.name:t)&&(u=e)}),{startIndex:a,dimensionsDefine:no(r),dimensionsDetectCount:o,potentialNameDimIndex:u}}function no(t){if(t){var e=N();return f(t,function(t,n){if(null==(t=o({},w(t)?t:{name:t})).name)return t;t.name+="",null==t.displayName&&(t.displayName=t.name);var i=e.get(t.name);return i?t.name+="-"+i.count++:e.set(t.name,{count:1}),t})}}function io(t,e,n,i){if(null==i&&(i=1/0),e===By)for(o=0;o<n.length&&o<i;o++)t(n[o]?n[o][0]:null,o);else for(var r=n[0]||[],o=0;o<r.length&&o<i;o++)t(r[o],o)}function ro(t){for(var e,n=0;n<t.length&&!(e=t[n++]););if(e){var i=[];return d(e,function(t,e){i.push(e)}),i}}function oo(t,e,n,i,r,o){var a=Yr(t),s={},l=[],u=[],h=t.subType,c=N(["pie","map","funnel"]),f=N(["line","bar","pictorialBar","scatter","effectScatter","candlestick","boxplot"]);if(a&&null!=f.get(h)){var p=t.ecModel,g=Vy(p).datasetMap,m=e.uid+"_"+r,v=g.get(m)||g.set(m,{categoryWayDim:1,valueWayDim:0});d(a.coordSysDims,function(t){if(null==a.firstCategoryDimIndex){e=v.valueWayDim++;s[t]=e,u.push(e)}else if(a.categoryAxisMap.get(t))s[t]=0,l.push(0);else{var e=v.categoryWayDim++;s[t]=e,u.push(e)}})}else if(null!=c.get(h)){for(var y,x=0;x<5&&null==y;x++)lo(n,i,r,o.dimensionsDefine,o.startIndex,x)||(y=x);if(null!=y){s.value=y;var _=o.potentialNameDimIndex||Math.max(y-1,0);u.push(_),l.push(_)}}return l.length&&(s.itemName=l),u.length&&(s.seriesName=u),s}function ao(t){var e=t.option;if(!e.data)return t.ecModel.getComponent("dataset",e.datasetIndex||0)}function so(t,e){return lo(t.data,t.sourceFormat,t.seriesLayoutBy,t.dimensionsDefine,t.startIndex,e)}function lo(t,e,n,i,r,o){function a(t){return(null==t||!isFinite(t)||""===t)&&(!(!_(t)||"-"===t)||void 0)}var s;if(M(t))return!1;var l;if(i&&(l=w(l=i[o])?l.name:l),e===Ly)if(n===By){for(var u=t[o],h=0;h<(u||[]).length&&h<5;h++)if(null!=(s=a(u[r+h])))return s}else for(h=0;h<t.length&&h<5;h++){var c=t[r+h];if(c&&null!=(s=a(c[o])))return s}else if(e===Oy){if(!l)return;for(h=0;h<t.length&&h<5;h++)if((d=t[h])&&null!=(s=a(d[l])))return s}else if(e===zy){if(!l)return;if(!(u=t[l])||M(u))return!1;for(h=0;h<u.length&&h<5;h++)if(null!=(s=a(u[h])))return s}else if(e===Py)for(h=0;h<t.length&&h<5;h++){var d=t[h],f=wn(d);if(!y(f))return!1;if(null!=(s=a(f[o])))return s}return!1}function uo(t,e){if(e){var n=e.seiresIndex,i=e.seriesId,r=e.seriesName;return null!=n&&t.componentIndex!==n||null!=i&&t.id!==i||null!=r&&t.name!==r}}function ho(t,e){var r=t.color&&!t.colorLayer;d(e,function(e,o){"colorLayer"===o&&r||Iy.hasClass(o)||("object"==typeof e?t[o]=t[o]?i(t[o],e,!1):n(e):null==t[o]&&(t[o]=e))})}function co(t){t=t,this.option={},this.option[Fy]=1,this._componentsMap=N({series:[]}),this._seriesIndices,this._seriesIndicesMap,ho(t,this._theme.option),i(t,Ty,!1),this.mergeOption(t)}function fo(t,e){y(e)||(e=e?[e]:[]);var n={};return d(e,function(e){n[e]=(t.get(e)||[]).slice()}),n}function po(t,e,n){return e.type?e.type:n?n.subType:Iy.determineSubType(t,e)}function go(t,e){t._seriesIndicesMap=N(t._seriesIndices=f(e,function(t){return t.componentIndex})||[])}function mo(t,e){return e.hasOwnProperty("subType")?g(t,function(t){return t.subType===e.subType}):t}function vo(t){d(Gy,function(e){this[e]=m(t[e],t)},this)}function yo(){this._coordinateSystems=[]}function xo(t){this._api=t,this._timelineOptions=[],this._mediaList=[],this._mediaDefault,this._currentMediaIndices=[],this._optionBackup,this._newBaseOption}function _o(t,e,n){var i,r,o=[],a=[],s=t.timeline;if(t.baseOption&&(r=t.baseOption),(s||t.options)&&(r=r||{},o=(t.options||[]).slice()),t.media){r=r||{};var l=t.media;Zy(l,function(t){t&&t.option&&(t.query?a.push(t):i||(i=t))})}return r||(r=t),r.timeline||(r.timeline=s),Zy([r].concat(o).concat(f(a,function(t){return t.option})),function(t){Zy(e,function(e){e(t,n)})}),{baseOption:r,timelineOptions:o,mediaDefault:i,mediaList:a}}function wo(t,e,n){var i={width:e,height:n,aspectratio:e/n},r=!0;return d(t,function(t,e){var n=e.match(Yy);if(n&&n[1]&&n[2]){var o=n[1],a=n[2].toLowerCase();bo(i[a],t,o)||(r=!1)}}),r}function bo(t,e,n){return"min"===n?t>=e:"max"===n?t<=e:t===e}function Mo(t,e){return t.join(",")===e.join(",")}function So(t,e){Zy(e=e||{},function(e,n){if(null!=e){var i=t[n];if(Iy.hasClass(n)){e=xn(e);var r=Mn(i=xn(i),e);t[n]=Xy(r,function(t){return t.option&&t.exist?jy(t.exist,t.option,!0):t.exist||t.option})}else t[n]=jy(i,e,!0)}})}function Io(t){var e=t&&t.itemStyle;if(e)for(var n=0,r=Ky.length;n<r;n++){var o=Ky[n],a=e.normal,s=e.emphasis;a&&a[o]&&(t[o]=t[o]||{},t[o].normal?i(t[o].normal,a[o]):t[o].normal=a[o],a[o]=null),s&&s[o]&&(t[o]=t[o]||{},t[o].emphasis?i(t[o].emphasis,s[o]):t[o].emphasis=s[o],s[o]=null)}}function Co(t,e,n){if(t&&t[e]&&(t[e].normal||t[e].emphasis)){var i=t[e].normal,r=t[e].emphasis;i&&(n?(t[e].normal=t[e].emphasis=null,a(t[e],i)):t[e]=i),r&&(t.emphasis=t.emphasis||{},t.emphasis[e]=r)}}function To(t){Co(t,"itemStyle"),Co(t,"lineStyle"),Co(t,"areaStyle"),Co(t,"label"),Co(t,"labelLine"),Co(t,"upperLabel"),Co(t,"edgeLabel")}function Do(t,e){var n=$y(t)&&t[e],i=$y(n)&&n.textStyle;if(i)for(var r=0,o=xm.length;r<o;r++){var e=xm[r];i.hasOwnProperty(e)&&(n[e]=i[e])}}function Ao(t){t&&(To(t),Do(t,"label"),t.emphasis&&Do(t.emphasis,"label"))}function ko(t){if($y(t)){Io(t),To(t),Do(t,"label"),Do(t,"upperLabel"),Do(t,"edgeLabel"),t.emphasis&&(Do(t.emphasis,"label"),Do(t.emphasis,"upperLabel"),Do(t.emphasis,"edgeLabel"));var e=t.markPoint;e&&(Io(e),Ao(e));var n=t.markLine;n&&(Io(n),Ao(n));var i=t.markArea;i&&Ao(i);var r=t.data;if("graph"===t.type){r=r||t.nodes;var o=t.links||t.edges;if(o&&!M(o))for(s=0;s<o.length;s++)Ao(o[s]);d(t.categories,function(t){To(t)})}if(r&&!M(r))for(s=0;s<r.length;s++)Ao(r[s]);if((e=t.markPoint)&&e.data)for(var a=e.data,s=0;s<a.length;s++)Ao(a[s]);if((n=t.markLine)&&n.data)for(var l=n.data,s=0;s<l.length;s++)y(l[s])?(Ao(l[s][0]),Ao(l[s][1])):Ao(l[s]);"gauge"===t.type?(Do(t,"axisLabel"),Do(t,"title"),Do(t,"detail")):"treemap"===t.type?(Co(t.breadcrumb,"itemStyle"),d(t.levels,function(t){To(t)})):"tree"===t.type&&To(t.leaves)}}function Po(t){return y(t)?t:t?[t]:[]}function Lo(t){return(y(t)?t[0]:t)||{}}function Oo(t,e){e=e.split(",");for(var n=t,i=0;i<e.length&&null!=(n=n&&n[e[i]]);i++);return n}function zo(t,e,n,i){e=e.split(",");for(var r,o=t,a=0;a<e.length-1;a++)null==o[r=e[a]]&&(o[r]={}),o=o[r];(i||null==o[e[a]])&&(o[e[a]]=n)}function Eo(t){d(Jy,function(e){e[0]in t&&!(e[1]in t)&&(t[e[1]]=t[e[0]])})}function No(t){d(t,function(e,n){var i=[],r=[NaN,NaN],o=[e.stackResultDimension,e.stackedOverDimension],a=e.data,s=e.isStackedByIndex,l=a.map(o,function(o,l,u){var h=a.get(e.stackedDimension,u);if(isNaN(h))return r;var c,d;s?d=a.getRawIndex(u):c=a.get(e.stackedByDimension,u);for(var f=NaN,p=n-1;p>=0;p--){var g=t[p];if(s||(d=g.data.rawIndexOf(g.stackedByDimension,c)),d>=0){var m=g.data.getByRawIndex(g.stackResultDimension,d);if(h>=0&&m>0||h<=0&&m<0){h+=m,f=m;break}}}return i[0]=h,i[1]=f,i});a.hostModel.setData(l),e.data=l})}function Ro(t,e){$r.isInstance(t)||(t=$r.seriesDataToSource(t)),this._source=t;var n=this._data=t.data,i=t.sourceFormat;i===Ny&&(this._offset=0,this._dimSize=e,this._data=n),o(this,ix[i===Ly?i+"_"+t.seriesLayoutBy:i])}function Bo(){return this._data.length}function Vo(t){return this._data[t]}function Fo(t){for(var e=0;e<t.length;e++)this._data.push(t[e])}function Ho(t,e,n,i){return null!=n?t[n]:t}function Go(t,e,n,i){return Wo(t[i],this._dimensionInfos[e])}function Wo(t,e){var n=e&&e.type;if("ordinal"===n){var i=e&&e.ordinalMeta;return i?i.parseAndCollect(t):t}return"time"===n&&"number"!=typeof t&&null!=t&&"-"!==t&&(t=+Ar(t)),null==t||""===t?NaN:+t}function Zo(t,e,n){if(t){var i=t.getRawDataItem(e);if(null!=i){var r,o,a=t.getProvider().getSource().sourceFormat,s=t.getDimensionInfo(n);return s&&(r=s.name,o=s.index),rx[a](i,e,o,r)}}}function Uo(t,e,n){if(t){var i=t.getProvider().getSource().sourceFormat;if(i===Py||i===Oy){var r=t.getRawDataItem(e);return i!==Py||w(r)||(r=null),r?r[n]:void 0}}}function Xo(t){return new jo(t)}function jo(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0,this.context}function Yo(t,e,n,i,r,o){ux.reset(n,i,r,o),t._callingProgress=e,t._callingProgress({start:n,end:i,count:i-n,next:ux.next},t.context)}function qo(t,e){t._dueIndex=t._outputDueEnd=t._dueEnd=0,t._settedOutputEnd=null;var n,i;!e&&t._reset&&((n=t._reset(t.context))&&n.progress&&(i=n.forceFirstProgress,n=n.progress),y(n)&&!n.length&&(n=null)),t._progress=n,t._modBy=t._modDataCount=null;var r=t._downstream;return r&&r.dirty(),i}function $o(t){var e=t.name;In(t)||(t.name=Ko(t)||e)}function Ko(t){var e=t.getRawData(),n=[];return d(e.mapDimension("seriesName",!0),function(t){var i=e.getDimensionInfo(t);i.displayName&&n.push(i.displayName)}),n.join(" ")}function Qo(t){return t.model.getRawData().count()}function Jo(t){var e=t.model;return e.setData(e.getRawData().cloneShallow()),ta}function ta(t,e){t.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function ea(t,e){d(t.CHANGABLE_METHODS,function(n){t.wrapMethod(n,v(na,e))})}function na(t){var e=ia(t);e&&e.setOutputEnd(this.count())}function ia(t){var e=(t.ecModel||{}).scheduler,n=e&&e.getPipeline(t.uid);if(n){var i=n.currentTask;if(i){var r=i.agentStubMap;r&&(i=r.get(t.uid))}return i}}function ra(){this.group=new Sg,this.uid=vr("viewChart"),this.renderTask=Xo({plan:sa,reset:la}),this.renderTask.context={view:this}}function oa(t,e){if(t&&(t.trigger(e),"group"===t.type))for(var n=0;n<t.childCount();n++)oa(t.childAt(n),e)}function aa(t,e,n){var i=Tn(t,e);null!=i?d(xn(i),function(e){oa(t.getItemGraphicEl(e),n)}):t.eachItemGraphicEl(function(t){oa(t,n)})}function sa(t){return mx(t.model)}function la(t){var e=t.model,n=t.ecModel,i=t.api,r=t.payload,o=e.pipelineContext.progressiveRender,a=t.view,s=r&&gx(r).updateMethod,l=o?"incrementalPrepareRender":s&&a[s]?s:"render";return"render"!==l&&a[l](e,n,i,r),yx[l]}function ua(t,e,n){function i(){h=(new Date).getTime(),c=null,t.apply(a,s||[])}var r,o,a,s,l,u=0,h=0,c=null;e=e||0;var d=function(){r=(new Date).getTime(),a=this,s=arguments;var t=l||e,d=l||n;l=null,o=r-(d?u:h)-t,clearTimeout(c),d?c=setTimeout(i,t):o>=0?i():c=setTimeout(i,-o),u=r};return d.clear=function(){c&&(clearTimeout(c),c=null)},d.debounceNextCall=function(t){l=t},d}function ha(t,e,n,i){var r=t[e];if(r){var o=r[xx]||r,a=r[bx];if(r[_x]!==n||a!==i){if(null==n||!i)return t[e]=o;(r=t[e]=ua(o,n,"debounce"===i))[xx]=o,r[bx]=i,r[_x]=n}return r}}function ca(t,e){var n=t[e];n&&n[xx]&&(t[e]=n[xx])}function da(t,e,n,i){this.ecInstance=t,this.api=e,this.unfinished;var n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice();this._allHandlers=n.concat(i),this._stageTaskMap=N()}function fa(t,e,n,i,r){function o(t,e){return t.setDirty&&(!t.dirtyMap||t.dirtyMap.get(e.__pipeline.id))}r=r||{};var a;d(e,function(e,s){if(!r.visualType||r.visualType===e.visualType){var l=t._stageTaskMap.get(e.uid),u=l.seriesTaskMap,h=l.overallTask;if(h){var c,d=h.agentStubMap;d.each(function(t){o(r,t)&&(t.dirty(),c=!0)}),c&&h.dirty(),Dx(h,i);var f=t.getPerformArgs(h,r.block);d.each(function(t){t.perform(f)}),a|=h.perform(f)}else u&&u.each(function(s,l){o(r,s)&&s.dirty();var u=t.getPerformArgs(s,r.block);u.skip=!e.performRawSeries&&n.isSeriesFiltered(s.context.model),Dx(s,i),a|=s.perform(u)})}}),t.unfinished|=a}function pa(t,e,n,i,r){function o(n){var o=n.uid,s=a.get(o)||a.set(o,Xo({plan:_a,reset:wa,count:Ma}));s.context={model:n,ecModel:i,api:r,useClearVisual:e.isVisual&&!e.isLayout,plan:e.plan,reset:e.reset,scheduler:t},Sa(t,n,s)}var a=n.seriesTaskMap||(n.seriesTaskMap=N()),s=e.seriesType,l=e.getTargetSeries;e.createOnAllSeries?i.eachRawSeries(o):s?i.eachRawSeriesByType(s,o):l&&l(i,r).each(o);var u=t._pipelineMap;a.each(function(t,e){u.get(e)||(t.dispose(),a.removeKey(e))})}function ga(t,e,n,i,r){function o(e){var n=e.uid,i=s.get(n);i||(i=s.set(n,Xo({reset:va,onDirty:xa})),a.dirty()),i.context={model:e,overallProgress:h,modifyOutputEnd:c},i.agent=a,i.__block=h,Sa(t,e,i)}var a=n.overallTask=n.overallTask||Xo({reset:ma});a.context={ecModel:i,api:r,overallReset:e.overallReset,scheduler:t};var s=a.agentStubMap=a.agentStubMap||N(),l=e.seriesType,u=e.getTargetSeries,h=!0,c=e.modifyOutputEnd;l?i.eachRawSeriesByType(l,o):u?u(i,r).each(o):(h=!1,d(i.getSeries(),o));var f=t._pipelineMap;s.each(function(t,e){f.get(e)||(t.dispose(),a.dirty(),s.removeKey(e))})}function ma(t){t.overallReset(t.ecModel,t.api,t.payload)}function va(t,e){return t.overallProgress&&ya}function ya(){this.agent.dirty(),this.getDownstream().dirty()}function xa(){this.agent&&this.agent.dirty()}function _a(t){return t.plan&&t.plan(t.model,t.ecModel,t.api,t.payload)}function wa(t){t.useClearVisual&&t.data.clearAllVisual();var e=t.resetDefines=xn(t.reset(t.model,t.ecModel,t.api,t.payload));return e.length>1?f(e,function(t,e){return ba(e)}):Ax}function ba(t){return function(e,n){var i=n.data,r=n.resetDefines[t];if(r&&r.dataEach)for(var o=e.start;o<e.end;o++)r.dataEach(i,o);else r&&r.progress&&r.progress(e,i)}}function Ma(t){return t.data.count()}function Sa(t,e,n){var i=e.uid,r=t._pipelineMap.get(i);!r.head&&(r.head=n),r.tail&&r.tail.pipe(n),r.tail=n,n.__idxInPipeline=r.count++,n.__pipeline=r}function Ia(t){kx=null;try{t(Px,Lx)}catch(t){}return kx}function Ca(t,e){for(var n in e.prototype)t[n]=R}function Ta(t){return function(e,n,i){e=e&&e.toLowerCase(),Zp.prototype[t].call(this,e,n,i)}}function Da(){Zp.call(this)}function Aa(t,e,i){function r(t,e){return t.__prio-e.__prio}i=i||{},"string"==typeof e&&(e=o_[e]),this.id,this.group,this._dom=t;var o=this._zr=mn(t,{renderer:i.renderer||"canvas",devicePixelRatio:i.devicePixelRatio,width:i.width,height:i.height});this._throttledZrFlush=ua(m(o.flush,o),17),(e=n(e))&&ex(e,!0),this._theme=e,this._chartsViews=[],this._chartsMap={},this._componentsViews=[],this._componentsMap={},this._coordSysMgr=new yo;var a=this._api=ja(this);te(r_,r),te(e_,r),this._scheduler=new da(this,a,e_,r_),Zp.call(this),this._messageCenter=new Da,this._initEvents(),this.resize=m(this.resize,this),this._pendingActions=[],o.animation.on("frame",this._onframe,this),Ra(o,this),O(this)}function ka(t,e,n){var i,r=this._model,o=this._coordSysMgr.getCoordinateSystems();e=An(r,e);for(var a=0;a<o.length;a++){var s=o[a];if(s[t]&&null!=(i=s[t](r,e,n)))return i}}function Pa(t){var e=t._model,n=t._scheduler;n.restorePipelines(e),n.prepareStageTasks(),Ba(t,"component",e,n),Ba(t,"chart",e,n),n.plan()}function La(t,e,n,i,r){function o(i){i&&i.__alive&&i[e]&&i[e](i.__model,a,t._api,n)}var a=t._model;if(i){var s={};s[i+"Id"]=n[i+"Id"],s[i+"Index"]=n[i+"Index"],s[i+"Name"]=n[i+"Name"];var l={mainType:i,query:s};r&&(l.subType=r);var u=n.excludeSeriesId;null!=u&&(u=N(xn(u))),a&&a.eachComponent(l,function(e){u&&null!=u.get(e.id)||o(t["series"===i?"_chartsMap":"_componentsMap"][e.__viewId])},t)}else Bx(t._componentsViews.concat(t._chartsViews),o)}function Oa(t,e){var n=t._chartsMap,i=t._scheduler;e.eachSeries(function(t){i.updateStreamModes(t,n[t.__viewId])})}function za(t,e){var n=t.type,i=t.escapeConnect,r=Jx[n],s=r.actionInfo,l=(s.update||"update").split(":"),u=l.pop();l=null!=l[0]&&Hx(l[0]),this[jx]=!0;var h=[t],c=!1;t.batch&&(c=!0,h=f(t.batch,function(e){return e=a(o({},e),t),e.batch=null,e}));var d,p=[],g="highlight"===n||"downplay"===n;Bx(h,function(t){d=r.action(t,this._model,this._api),(d=d||o({},t)).type=s.event||d.type,p.push(d),g?La(this,u,t,"series"):l&&La(this,u,t,l.main,l.sub)},this),"none"===u||g||l||(this[Yx]?(Pa(this),Kx.update.call(this,t),this[Yx]=!1):Kx[u].call(this,t)),d=c?{type:s.event||n,escapeConnect:i,batch:p}:p[0],this[jx]=!1,!e&&this._messageCenter.trigger(d.type,d)}function Ea(t){for(var e=this._pendingActions;e.length;){var n=e.shift();za.call(this,n,t)}}function Na(t){!t&&this.trigger("updated")}function Ra(t,e){t.on("rendered",function(){e.trigger("rendered"),!t.animation.isFinished()||e[Yx]||e._scheduler.unfinished||e._pendingActions.length||e.trigger("finished")})}function Ba(t,e,n,i){function r(t){var e="_ec_"+t.id+"_"+t.type,r=s[e];if(!r){var h=Hx(t.type);(r=new(o?dx.getClass(h.main,h.sub):ra.getClass(h.sub))).init(n,u),s[e]=r,a.push(r),l.add(r.group)}t.__viewId=r.__id=e,r.__alive=!0,r.__model=t,r.group.__ecComponentInfo={mainType:t.mainType,index:t.componentIndex},!o&&i.prepareView(r,t,n,u)}for(var o="component"===e,a=o?t._componentsViews:t._chartsViews,s=o?t._componentsMap:t._chartsMap,l=t._zr,u=t._api,h=0;h<a.length;h++)a[h].__alive=!1;o?n.eachComponent(function(t,e){"series"!==t&&r(e)}):n.eachSeries(r);for(h=0;h<a.length;){var c=a[h];c.__alive?h++:(!o&&c.renderTask.dispose(),l.remove(c.group),c.dispose(n,u),a.splice(h,1),delete s[c.__id],c.__id=c.group.__ecComponentInfo=null)}}function Va(t){t.clearColorPalette(),t.eachSeries(function(t){t.clearColorPalette()})}function Fa(t,e,n,i){Ha(t,e,n,i),Bx(t._chartsViews,function(t){t.__alive=!1}),Ga(t,e,n,i),Bx(t._chartsViews,function(t){t.__alive||t.remove(e,n)})}function Ha(t,e,n,i,r){Bx(r||t._componentsViews,function(t){var r=t.__model;t.render(r,e,n,i),Xa(r,t)})}function Ga(t,e,n,i,r){var o,a=t._scheduler;e.eachSeries(function(e){var n=t._chartsMap[e.__viewId];n.__alive=!0;var s=n.renderTask;a.updatePayload(s,i),r&&r.get(e.uid)&&s.dirty(),o|=s.perform(a.getPerformArgs(s)),n.group.silent=!!e.get("silent"),Xa(e,n),Ua(e,n)}),a.unfinished|=o,Za(t._zr,e),Ix(t._zr.dom,e)}function Wa(t,e){Bx(i_,function(n){n(t,e)})}function Za(t,e){var n=t.storage,i=0;n.traverse(function(t){t.isGroup||i++}),i>e.get("hoverLayerThreshold")&&!bp.node&&n.traverse(function(t){t.isGroup||(t.useHoverLayer=!0)})}function Ua(t,e){var n=t.get("blendMode")||null;e.group.traverse(function(t){t.isGroup||t.style.blend!==n&&t.setStyle("blend",n),t.eachPendingDisplayable&&t.eachPendingDisplayable(function(t){t.setStyle("blend",n)})})}function Xa(t,e){var n=t.get("z"),i=t.get("zlevel");e.group.traverse(function(t){"group"!==t.type&&(null!=n&&(t.z=n),null!=i&&(t.zlevel=i))})}function ja(t){var e=t._coordSysMgr;return o(new vo(t),{getCoordinateSystems:m(e.getCoordinateSystems,e),getComponentByElement:function(e){for(;e;){var n=e.__ecComponentInfo;if(null!=n)return t._model.getComponent(n.mainType,n.index);e=e.parent}}})}function Ya(t){function e(t,e){for(var i=0;i<t.length;i++)t[i][n]=e}var n="__connectUpdateStatus";Bx(t_,function(i,r){t._messageCenter.on(r,function(i){if(l_[t.group]&&0!==t[n]){if(i&&i.escapeConnect)return;var r=t.makeActionFromEvent(i),o=[];Bx(s_,function(e){e!==t&&e.group===t.group&&o.push(e)}),e(o,0),Bx(o,function(t){1!==t[n]&&t.dispatchAction(r)}),e(o,2)}})})}function qa(t){l_[t]=!1}function $a(t){return s_[Ln(t,c_)]}function Ka(t,e){o_[t]=e}function Qa(t){n_.push(t)}function Ja(t,e){is(e_,t,e,Wx)}function ts(t,e,n){"function"==typeof e&&(n=e,e="");var i=Fx(t)?t.type:[t,t={event:e}][0];t.event=(t.event||i).toLowerCase(),e=t.event,Rx(qx.test(i)&&qx.test(e)),Jx[i]||(Jx[i]={action:n,actionInfo:t}),t_[e]=i}function es(t,e){is(r_,t,e,Zx,"layout")}function ns(t,e){is(r_,t,e,Ux,"visual")}function is(t,e,n,i,r){(Vx(e)||Fx(e))&&(n=e,e=i);var o=da.wrapStageHandler(n,r);return o.__prio=e,o.__raw=n,t.push(o),o}function rs(t,e){a_[t]=e}function os(t){return Iy.extend(t)}function as(t){return dx.extend(t)}function ss(t){return cx.extend(t)}function ls(t){return ra.extend(t)}function us(t){return t}function hs(t,e,n,i,r){this._old=t,this._new=e,this._oldKeyGetter=n||us,this._newKeyGetter=i||us,this.context=r}function cs(t,e,n,i,r){for(var o=0;o<t.length;o++){var a="_ec_"+r[i](t[o],o),s=e[a];null==s?(n.push(a),e[a]=o):(s.length||(e[a]=s=[s]),s.push(o))}}function ds(t){var e={},n=e.encode={},i=N(),r=[],o=[];d(t.dimensions,function(e){var a=t.getDimensionInfo(e),s=a.coordDim;if(s){var l=n[s];n.hasOwnProperty(s)||(l=n[s]=[]),l[a.coordDimIndex]=e,a.isExtraCoord||(i.set(s,1),ps(a.type)&&(r[0]=e)),a.defaultTooltip&&o.push(e)}g_.each(function(t,e){var i=n[e];n.hasOwnProperty(e)||(i=n[e]=[]);var r=a.otherDims[e];null!=r&&!1!==r&&(i[r]=a.name)})});var a=[],s={};i.each(function(t,e){var i=n[e];s[e]=i[0],a=a.concat(i)}),e.dataDimsOnCoord=a,e.encodeFirstDimNotExtra=s;var l=n.label;l&&l.length&&(r=l.slice());var u=n.tooltip;return u&&u.length?o=u.slice():o.length||(o=r.slice()),n.defaultedLabel=r,n.defaultedTooltip=o,e}function fs(t){return"category"===t?"ordinal":"time"===t?"time":"float"}function ps(t){return!("ordinal"===t||"time"===t)}function gs(t){return t._rawCount>65535?x_:__}function ms(t){var e=t.constructor;return e===Array?t.slice():new e(t)}function vs(t,e){d(w_.concat(e.__wrappedMethods||[]),function(n){e.hasOwnProperty(n)&&(t[n]=e[n])}),t.__wrappedMethods=e.__wrappedMethods,d(b_,function(i){t[i]=n(e[i])}),t._calculationInfo=o(e._calculationInfo)}function ys(t){var e=t._invertedIndicesMap;d(e,function(n,i){var r=t._dimensionInfos[i].ordinalMeta;if(r){n=e[i]=new x_(r.categories.length);for(o=0;o<n.length;o++)n[o]=NaN;for(var o=0;o<t._count;o++)n[t.get(i,o)]=o}})}function xs(t,e,n){var i;if(null!=e){var r=t._chunkSize,o=Math.floor(n/r),a=n%r,s=t.dimensions[e],l=t._storage[s][o];if(l){i=l[a];var u=t._dimensionInfos[s].ordinalMeta;u&&u.categories.length&&(i=u.categories[i])}}return i}function _s(t){return t}function ws(t){return t<this._count&&t>=0?this._indices[t]:-1}function bs(t,e){var n=t._idList[e];return null==n&&(n=xs(t,t._idDimIdx,e)),null==n&&(n=v_+e),n}function Ms(t){return y(t)||(t=[t]),t}function Ss(t,e){var n=t.dimensions,i=new M_(f(n,t.getDimensionInfo,t),t.hostModel);vs(i,t);for(var r=i._storage={},o=t._storage,a=0;a<n.length;a++){var s=n[a];o[s]&&(l(e,s)>=0?(r[s]=Is(o[s]),i._rawExtent[s]=Cs(),i._extent[s]=null):r[s]=o[s])}return i}function Is(t){for(var e=new Array(t.length),n=0;n<t.length;n++)e[n]=ms(t[n]);return e}function Cs(){return[1/0,-1/0]}function Ts(t,e,i){function r(t,e,n){null!=g_.get(e)?t.otherDims[e]=n:(t.coordDim=e,t.coordDimIndex=n,h.set(e,!0))}$r.isInstance(e)||(e=$r.seriesDataToSource(e)),i=i||{},t=(t||[]).slice();for(var s=(i.dimsDef||[]).slice(),l=N(i.encodeDef),u=N(),h=N(),c=[],f=Ds(e,t,s,i.dimCount),p=0;p<f;p++){var g=s[p]=o({},w(s[p])?s[p]:{name:s[p]}),m=g.name,v=c[p]={otherDims:{}};null!=m&&null==u.get(m)&&(v.name=v.displayName=m,u.set(m,p)),null!=g.type&&(v.type=g.type),null!=g.displayName&&(v.displayName=g.displayName)}l.each(function(t,e){t=xn(t).slice();var n=l.set(e,[]);d(t,function(t,i){_(t)&&(t=u.get(t)),null!=t&&t<f&&(n[i]=t,r(c[t],e,i))})});var y=0;d(t,function(t,e){var i,t,o,s;if(_(t))i=t,t={};else{i=t.name;var u=t.ordinalMeta;t.ordinalMeta=null,(t=n(t)).ordinalMeta=u,o=t.dimsDef,s=t.otherDims,t.name=t.coordDim=t.coordDimIndex=t.dimsDef=t.otherDims=null}var h=xn(l.get(i));if(!h.length)for(var f=0;f<(o&&o.length||1);f++){for(;y<c.length&&null!=c[y].coordDim;)y++;y<c.length&&h.push(y++)}d(h,function(e,n){var l=c[e];if(r(a(l,t),i,n),null==l.name&&o){var u=o[n];!w(u)&&(u={name:u}),l.name=l.displayName=u.name,l.defaultTooltip=u.defaultTooltip}s&&a(l.otherDims,s)})});var x=i.generateCoord,b=i.generateCoordCount,M=null!=b;b=x?b||1:0;for(var S=x||"value",I=0;I<f;I++)null==(v=c[I]=c[I]||{}).coordDim&&(v.coordDim=As(S,h,M),v.coordDimIndex=0,(!x||b<=0)&&(v.isExtraCoord=!0),b--),null==v.name&&(v.name=As(v.coordDim,u)),null==v.type&&so(e,I,v.name)&&(v.type="ordinal");return c}function Ds(t,e,n,i){var r=Math.max(t.dimensionsDetectCount||1,e.length,n.length,i||0);return d(e,function(t){var e=t.dimsDef;e&&(r=Math.max(r,e.length))}),r}function As(t,e,n){if(n||null!=e.get(t)){for(var i=0;null!=e.get(t+i);)i++;t+=i}return e.set(t,!0),t}function ks(t,e,n){var i,r,o,a,s=(n=n||{}).byIndex,l=n.stackedCoordDimension,u=!(!t||!t.get("stack"));if(d(e,function(t,n){_(t)&&(e[n]=t={name:t}),u&&!t.isExtraCoord&&(s||i||!t.ordinalMeta||(i=t),r||"ordinal"===t.type||"time"===t.type||l&&l!==t.coordDim||(r=t))}),!r||s||i||(s=!0),r){o="__\0ecstackresult",a="__\0ecstackedover",i&&(i.createInvertedIndices=!0);var h=r.coordDim,c=r.type,f=0;d(e,function(t){t.coordDim===h&&f++}),e.push({name:o,coordDim:h,coordDimIndex:f,type:c,isExtraCoord:!0,isCalculationCoord:!0}),f++,e.push({name:a,coordDim:a,coordDimIndex:f,type:c,isExtraCoord:!0,isCalculationCoord:!0})}return{stackedDimension:r&&r.name,stackedByDimension:i&&i.name,isStackedByIndex:s,stackedOverDimension:a,stackResultDimension:o}}function Ps(t,e){return!!e&&e===t.getCalculationInfo("stackedDimension")}function Ls(t,e){return Ps(t,e)?t.getCalculationInfo("stackResultDimension"):e}function Os(t,e,n){n=n||{},$r.isInstance(t)||(t=$r.seriesDataToSource(t));var i,r=e.get("coordinateSystem"),o=yo.get(r),a=Yr(e);a&&(i=f(a.coordSysDims,function(t){var e={name:t},n=a.axisMap.get(t);if(n){var i=n.get("type");e.type=fs(i)}return e})),i||(i=o&&(o.getDimensionsInfo?o.getDimensionsInfo():o.dimensions.slice())||["x","y"]);var s,l,u=C_(t,{coordDimensions:i,generateCoord:n.generateCoord});a&&d(u,function(t,e){var n=t.coordDim,i=a.categoryAxisMap.get(n);i&&(null==s&&(s=e),t.ordinalMeta=i.getOrdinalMeta()),null!=t.otherDims.itemName&&(l=!0)}),l||null==s||(u[s].otherDims.itemName=0);var h=ks(e,u),c=new M_(u,e);c.setCalculationInfo(h);var p=null!=s&&zs(t)?function(t,e,n,i){return i===s?n:this.defaultDimValueGetter(t,e,n,i)}:null;return c.hasItemOption=!1,c.initData(t,null,p),c}function zs(t){if(t.sourceFormat===Py){var e=Es(t.data||[]);return null!=e&&!y(wn(e))}}function Es(t){for(var e=0;e<t.length&&null==t[e];)e++;return t[e]}function Ns(t){this._setting=t||{},this._extent=[1/0,-1/0],this._interval=0,this.init&&this.init.apply(this,arguments)}function Rs(t){this.categories=t.categories||[],this._needCollect=t.needCollect,this._deduplication=t.deduplication,this._map}function Bs(t){return t._map||(t._map=N(t.categories))}function Vs(t){return w(t)&&null!=t.value?t.value:t+""}function Fs(t,e,n,i){var r={},o=t[1]-t[0],a=r.interval=Lr(o/e,!0);null!=n&&a<n&&(a=r.interval=n),null!=i&&a>i&&(a=r.interval=i);var s=r.intervalPrecision=Hs(a);return Ws(r.niceTickExtent=[k_(Math.ceil(t[0]/a)*a,s),k_(Math.floor(t[1]/a)*a,s)],t),r}function Hs(t){return Sr(t)+2}function Gs(t,e,n){t[e]=Math.max(Math.min(t[e],n[1]),n[0])}function Ws(t,e){!isFinite(t[0])&&(t[0]=e[0]),!isFinite(t[1])&&(t[1]=e[1]),Gs(t,0,e),Gs(t,1,e),t[0]>t[1]&&(t[0]=t[1])}function Zs(t,e,n,i){var r=[];if(!t)return r;e[0]<n[0]&&r.push(e[0]);for(var o=n[0];o<=n[1]&&(r.push(o),(o=k_(o+t,i))!==r[r.length-1]);)if(r.length>1e4)return[];return e[1]>(r.length?r[r.length-1]:n[1])&&r.push(e[1]),r}function Us(t){return t.get("stack")||O_+t.seriesIndex}function Xs(t){return t.dim+t.index}function js(t,e){var n=[];return e.eachSeriesByType(t,function(t){Ks(t)&&!Qs(t)&&n.push(t)}),n}function Ys(t){var e=[];return d(t,function(t){var n=t.getData(),i=t.coordinateSystem.getBaseAxis(),r=i.getExtent(),o="category"===i.type?i.getBandWidth():Math.abs(r[1]-r[0])/n.count(),a=_r(t.get("barWidth"),o),s=_r(t.get("barMaxWidth"),o),l=t.get("barGap"),u=t.get("barCategoryGap");e.push({bandWidth:o,barWidth:a,barMaxWidth:s,barGap:l,barCategoryGap:u,axisKey:Xs(i),stackId:Us(t)})}),qs(e)}function qs(t){var e={};d(t,function(t,n){var i=t.axisKey,r=t.bandWidth,o=e[i]||{bandWidth:r,remainedWidth:r,autoWidthCount:0,categoryGap:"20%",gap:"30%",stacks:{}},a=o.stacks;e[i]=o;var s=t.stackId;a[s]||o.autoWidthCount++,a[s]=a[s]||{width:0,maxWidth:0};var l=t.barWidth;l&&!a[s].width&&(a[s].width=l,l=Math.min(o.remainedWidth,l),o.remainedWidth-=l);var u=t.barMaxWidth;u&&(a[s].maxWidth=u);var h=t.barGap;null!=h&&(o.gap=h);var c=t.barCategoryGap;null!=c&&(o.categoryGap=c)});var n={};return d(e,function(t,e){n[e]={};var i=t.stacks,r=t.bandWidth,o=_r(t.categoryGap,r),a=_r(t.gap,1),s=t.remainedWidth,l=t.autoWidthCount,u=(s-o)/(l+(l-1)*a);u=Math.max(u,0),d(i,function(t,e){var n=t.maxWidth;n&&n<u&&(n=Math.min(n,s),t.width&&(n=Math.min(n,t.width)),s-=n,t.width=n,l--)}),u=(s-o)/(l+(l-1)*a),u=Math.max(u,0);var h,c=0;d(i,function(t,e){t.width||(t.width=u),h=t,c+=t.width*(1+a)}),h&&(c-=h.width*a);var f=-c/2;d(i,function(t,i){n[e][i]=n[e][i]||{offset:f,width:t.width},f+=t.width*(1+a)})}),n}function $s(t,e,n){if(t&&e){var i=t[Xs(e)];return null!=i&&null!=n&&(i=i[Us(n)]),i}}function Ks(t){return t.coordinateSystem&&"cartesian2d"===t.coordinateSystem.type}function Qs(t){return t.pipelineContext&&t.pipelineContext.large}function Js(t,e,n){return l(t.getAxesOnZeroOf(),e)>=0||n?e.toGlobalCoord(e.dataToCoord(0)):e.getGlobalExtent()[0]}function tl(t,e){return U_(t,Z_(e))}function el(t,e){var n,i,r,o=t.type,a=e.getMin(),s=e.getMax(),l=null!=a,u=null!=s,h=t.getExtent();"ordinal"===o?n=e.getCategories().length:(y(i=e.get("boundaryGap"))||(i=[i||0,i||0]),"boolean"==typeof i[0]&&(i=[0,0]),i[0]=_r(i[0],1),i[1]=_r(i[1],1),r=h[1]-h[0]||Math.abs(h[0])),null==a&&(a="ordinal"===o?n?0:NaN:h[0]-i[0]*r),null==s&&(s="ordinal"===o?n?n-1:NaN:h[1]+i[1]*r),"dataMin"===a?a=h[0]:"function"==typeof a&&(a=a({min:h[0],max:h[1]})),"dataMax"===s?s=h[1]:"function"==typeof s&&(s=s({min:h[0],max:h[1]})),(null==a||!isFinite(a))&&(a=NaN),(null==s||!isFinite(s))&&(s=NaN),t.setBlank(I(a)||I(s)||"ordinal"===o&&!t.getOrdinalMeta().categories.length),e.getNeedCrossZero()&&(a>0&&s>0&&!l&&(a=0),a<0&&s<0&&!u&&(s=0));var c=e.ecModel;if(c&&"time"===o){var f,p=js("bar",c);if(d(p,function(t){f|=t.getBaseAxis()===e.axis}),f){var g=Ys(p),m=nl(a,s,e,g);a=m.min,s=m.max}}return[a,s]}function nl(t,e,n,i){var r=n.axis.getExtent(),o=r[1]-r[0],a=$s(i,n.axis);if(void 0===a)return{min:t,max:e};var s=1/0;d(a,function(t){s=Math.min(t.offset,s)});var l=-1/0;d(a,function(t){l=Math.max(t.offset+t.width,l)}),s=Math.abs(s),l=Math.abs(l);var u=s+l,h=e-t,c=h/(1-(s+l)/o)-h;return e+=c*(l/u),t-=c*(s/u),{min:t,max:e}}function il(t,e){var n=el(t,e),i=null!=e.getMin(),r=null!=e.getMax(),o=e.get("splitNumber");"log"===t.type&&(t.base=e.get("logBase"));var a=t.type;t.setExtent(n[0],n[1]),t.niceExtent({splitNumber:o,fixMin:i,fixMax:r,minInterval:"interval"===a||"time"===a?e.get("minInterval"):null,maxInterval:"interval"===a||"time"===a?e.get("maxInterval"):null});var s=e.get("interval");null!=s&&t.setInterval&&t.setInterval(s)}function rl(t,e){if(e=e||t.get("type"))switch(e){case"category":return new A_(t.getOrdinalMeta?t.getOrdinalMeta():t.getCategories(),[1/0,-1/0]);case"value":return new L_;default:return(Ns.getClass(e)||L_).create(t)}}function ol(t){var e=t.scale.getExtent(),n=e[0],i=e[1];return!(n>0&&i>0||n<0&&i<0)}function al(t){var e=t.getLabelModel().get("formatter"),n="category"===t.type?t.scale.getExtent()[0]:null;return"string"==typeof e?e=function(t){return function(e){return t.replace("{value}",null!=e?e:"")}}(e):"function"==typeof e?function(i,r){return null!=n&&(r=i-n),e(sl(t,i),r)}:function(e){return t.scale.getLabel(e)}}function sl(t,e){return"category"===t.type?t.scale.getLabel(e):e}function ll(t){var e=t.model,n=t.scale;if(e.get("axisLabel.show")&&!n.isBlank()){var i,r,o="category"===t.type,a=n.getExtent();r=o?n.count():(i=n.getTicks()).length;var s,l=t.getLabelModel(),u=al(t),h=1;r>40&&(h=Math.ceil(r/40));for(var c=0;c<r;c+=h){var d=u(i?i[c]:a[0]+c),f=ul(l.getTextRect(d),l.get("rotate")||0);s?s.union(f):s=f}return s}}function ul(t,e){var n=e*Math.PI/180,i=t.plain(),r=i.width,o=i.height,a=r*Math.cos(n)+o*Math.sin(n),s=r*Math.sin(n)+o*Math.cos(n);return new Xt(i.x,i.y,a,s)}function hl(t,e){if("image"!==this.type){var n=this.style,i=this.shape;i&&"line"===i.symbolType?n.stroke=t:this.__isEmptyBrush?(n.stroke=t,n.fill=e||"#fff"):(n.fill&&(n.fill=t),n.stroke&&(n.stroke=t)),this.dirty(!1)}}function cl(t,e,n,i,r,o,a){var s=0===t.indexOf("empty");s&&(t=t.substr(5,1).toLowerCase()+t.substr(6));var l;return l=0===t.indexOf("image://")?Pi(t.slice(8),new Xt(e,n,i,r),a?"center":"cover"):0===t.indexOf("path://")?ki(t.slice(7),{},new Xt(e,n,i,r),a?"center":"cover"):new rw({shape:{symbolType:t,x:e,y:n,width:i,height:r}}),l.__isEmptyBrush=s,l.setColor=hl,l.setColor(o),l}function dl(t,e){return Math.abs(t-e)<sw}function fl(t,e,n){var i=0,r=t[0];if(!r)return!1;for(var o=1;o<t.length;o++){var a=t[o];i+=hi(r[0],r[1],a[0],a[1],e,n),r=a}var s=t[0];return dl(r[0],s[0])&&dl(r[1],s[1])||(i+=hi(r[0],r[1],s[0],s[1],e,n)),0!==i}function pl(t,e,n){if(this.name=t,this.geometries=e,n)n=[n[0],n[1]];else{var i=this.getBoundingRect();n=[i.x+i.width/2,i.y+i.height/2]}this.center=n}function gl(t){if(!t.UTF8Encoding)return t;var e=t.UTF8Scale;null==e&&(e=1024);for(var n=t.features,i=0;i<n.length;i++)for(var r=n[i].geometry,o=r.coordinates,a=r.encodeOffsets,s=0;s<o.length;s++){var l=o[s];if("Polygon"===r.type)o[s]=ml(l,a[s],e);else if("MultiPolygon"===r.type)for(var u=0;u<l.length;u++){var h=l[u];l[u]=ml(h,a[s][u],e)}}return t.UTF8Encoding=!1,t}function ml(t,e,n){for(var i=[],r=e[0],o=e[1],a=0;a<t.length;a+=2){var s=t.charCodeAt(a)-64,l=t.charCodeAt(a+1)-64;s=s>>1^-(1&s),l=l>>1^-(1&l),r=s+=r,o=l+=o,i.push([s/n,l/n])}return i}function vl(t){return"category"===t.type?xl(t):bl(t)}function yl(t,e){return"category"===t.type?wl(t,e):{ticks:t.scale.getTicks()}}function xl(t){var e=t.getLabelModel(),n=_l(t,e);return!e.get("show")||t.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}function _l(t,e){var n=Ml(t,"labels"),i=Pl(e),r=Sl(n,i);if(r)return r;var o,a;return o=x(i)?kl(t,i):Al(t,a="auto"===i?Cl(t):i),Il(n,i,{labels:o,labelCategoryInterval:a})}function wl(t,e){var n=Ml(t,"ticks"),i=Pl(e),r=Sl(n,i);if(r)return r;var o,a;if(e.get("show")&&!t.scale.isBlank()||(o=[]),x(i))o=kl(t,i,!0);else if("auto"===i){var s=_l(t,t.getLabelModel());a=s.labelCategoryInterval,o=f(s.labels,function(t){return t.tickValue})}else o=Al(t,a=i,!0);return Il(n,i,{ticks:o,tickCategoryInterval:a})}function bl(t){var e=t.scale.getTicks(),n=al(t);return{labels:f(e,function(e,i){return{formattedLabel:n(e,i),rawLabel:t.scale.getLabel(e),tickValue:e}})}}function Ml(t,e){return uw(t)[e]||(uw(t)[e]=[])}function Sl(t,e){for(var n=0;n<t.length;n++)if(t[n].key===e)return t[n].value}function Il(t,e,n){return t.push({key:e,value:n}),n}function Cl(t){var e=uw(t).autoInterval;return null!=e?e:uw(t).autoInterval=t.calculateCategoryInterval()}function Tl(t){var e=Dl(t),n=al(t),i=(e.axisRotate-e.labelRotate)/180*Math.PI,r=t.scale,o=r.getExtent(),a=r.count();if(o[1]-o[0]<1)return 0;var s=1;a>40&&(s=Math.max(1,Math.floor(a/40)));for(var l=o[0],u=t.dataToCoord(l+1)-t.dataToCoord(l),h=Math.abs(u*Math.cos(i)),c=Math.abs(u*Math.sin(i)),d=0,f=0;l<=o[1];l+=s){var p=0,g=0,m=ce(n(l),e.font,"center","top");p=1.3*m.width,g=1.3*m.height,d=Math.max(d,p,7),f=Math.max(f,g,7)}var v=d/h,y=f/c;isNaN(v)&&(v=1/0),isNaN(y)&&(y=1/0);var x=Math.max(0,Math.floor(Math.min(v,y))),_=uw(t.model),w=_.lastAutoInterval,b=_.lastTickCount;return null!=w&&null!=b&&Math.abs(w-x)<=1&&Math.abs(b-a)<=1&&w>x?x=w:(_.lastTickCount=a,_.lastAutoInterval=x),x}function Dl(t){var e=t.getLabelModel();return{axisRotate:t.getRotate?t.getRotate():t.isHorizontal&&!t.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}function Al(t,e,n){function i(t){l.push(n?t:{formattedLabel:r(t),rawLabel:o.getLabel(t),tickValue:t})}var r=al(t),o=t.scale,a=o.getExtent(),s=t.getLabelModel(),l=[],u=Math.max((e||0)+1,1),h=a[0],c=o.count();0!==h&&u>1&&c/u>2&&(h=Math.round(Math.ceil(h/u)*u));var d={min:s.get("showMinLabel"),max:s.get("showMaxLabel")};d.min&&h!==a[0]&&i(a[0]);for(var f=h;f<=a[1];f+=u)i(f);return d.max&&f!==a[1]&&i(a[1]),l}function kl(t,e,n){var i=t.scale,r=al(t),o=[];return d(i.getTicks(),function(t){var a=i.getLabel(t);e(t,a)&&o.push(n?t:{formattedLabel:r(t),rawLabel:a,tickValue:t})}),o}function Pl(t){var e=t.get("interval");return null==e?"auto":e}function Ll(t,e){var n=(t[1]-t[0])/e/2;t[0]+=n,t[1]-=n}function Ol(t,e,n,i,r){function o(t,e){return h?t>e:t<e}var a=e.length;if(t.onBand&&!i&&a){var s,l=t.getExtent();if(1===a)e[0].coord=l[0],s=e[1]={coord:l[0]};else{var u=e[1].coord-e[0].coord;d(e,function(t){t.coord-=u/2;var e=e||0;e%2>0&&(t.coord-=u/(2*(e+1)))}),s={coord:e[a-1].coord+u},e.push(s)}var h=l[0]>l[1];o(e[0].coord,l[0])&&(r?e[0].coord=l[0]:e.shift()),r&&o(l[0],e[0].coord)&&e.unshift({coord:l[0]}),o(l[1],s.coord)&&(r?s.coord=l[1]:e.pop()),r&&o(s.coord,l[1])&&e.push({coord:l[1]})}}function zl(t,e){var n=t.mapDimension("defaultedLabel",!0),i=n.length;if(1===i)return Zo(t,e,n[0]);if(i){for(var r=[],o=0;o<n.length;o++){var a=Zo(t,e,n[o]);r.push(a)}return r.join(" ")}}function El(t,e,n){Sg.call(this),this.updateData(t,e,n)}function Nl(t){return[t[0]/2,t[1]/2]}function Rl(t,e){this.parent.drift(t,e)}function Bl(t){this.group=new Sg,this._symbolCtor=t||El}function Vl(t,e,n,i){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(e[0],e[1]))&&"none"!==t.getItemVisual(n,"symbol")}function Fl(t){return null==t||w(t)||(t={isIgnore:t}),t||{}}function Hl(t){var e=t.hostModel;return{itemStyle:e.getModel("itemStyle").getItemStyle(["color"]),hoverItemStyle:e.getModel("emphasis.itemStyle").getItemStyle(),symbolRotate:e.get("symbolRotate"),symbolOffset:e.get("symbolOffset"),hoverAnimation:e.get("hoverAnimation"),labelModel:e.getModel("label"),hoverLabelModel:e.getModel("emphasis.label"),cursorStyle:e.get("cursor")}}function Gl(t,e,n){var i,r=t.getBaseAxis(),o=t.getOtherAxis(r),a=Wl(o,n),s=r.dim,l=o.dim,u=e.mapDimension(l),h=e.mapDimension(s),c="x"===l||"radius"===l?1:0,d=f(t.dimensions,function(t){return e.mapDimension(t)}),p=e.getCalculationInfo("stackResultDimension");return(i|=Ps(e,d[0]))&&(d[0]=p),(i|=Ps(e,d[1]))&&(d[1]=p),{dataDimsForPoint:d,valueStart:a,valueAxisDim:l,baseAxisDim:s,stacked:!!i,valueDim:u,baseDim:h,baseDataOffset:c,stackedOverDimension:e.getCalculationInfo("stackedOverDimension")}}function Wl(t,e){var n=0,i=t.scale.getExtent();return"start"===e?n=i[0]:"end"===e?n=i[1]:i[0]>0?n=i[0]:i[1]<0&&(n=i[1]),n}function Zl(t,e,n,i){var r=NaN;t.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(r)&&(r=t.valueStart);var o=t.baseDataOffset,a=[];return a[o]=n.get(t.baseDim,i),a[1-o]=r,e.dataToPoint(a)}function Ul(t,e){var n=[];return e.diff(t).add(function(t){n.push({cmd:"+",idx:t})}).update(function(t,e){n.push({cmd:"=",idx:e,idx1:t})}).remove(function(t){n.push({cmd:"-",idx:t})}).execute(),n}function Xl(t){return isNaN(t[0])||isNaN(t[1])}function jl(t,e,n,i,r,o,a,s,l,u,h){return"none"!==u&&u?Yl.apply(this,arguments):ql.apply(this,arguments)}function Yl(t,e,n,i,r,o,a,s,l,u,h){for(var c=0,d=n,f=0;f<i;f++){var p=e[d];if(d>=r||d<0)break;if(Xl(p)){if(h){d+=o;continue}break}if(d===n)t[o>0?"moveTo":"lineTo"](p[0],p[1]);else if(l>0){var g=e[c],m="y"===u?1:0,v=(p[m]-g[m])*l;Iw(Tw,g),Tw[m]=g[m]+v,Iw(Dw,p),Dw[m]=p[m]-v,t.bezierCurveTo(Tw[0],Tw[1],Dw[0],Dw[1],p[0],p[1])}else t.lineTo(p[0],p[1]);c=d,d+=o}return f}function ql(t,e,n,i,r,o,a,s,l,u,h){for(var c=0,d=n,f=0;f<i;f++){var p=e[d];if(d>=r||d<0)break;if(Xl(p)){if(h){d+=o;continue}break}if(d===n)t[o>0?"moveTo":"lineTo"](p[0],p[1]),Iw(Tw,p);else if(l>0){var g=d+o,m=e[g];if(h)for(;m&&Xl(e[g]);)m=e[g+=o];var v=.5,y=e[c];if(!(m=e[g])||Xl(m))Iw(Dw,p);else{Xl(m)&&!h&&(m=p),W(Cw,m,y);var x,_;if("x"===u||"y"===u){var w="x"===u?0:1;x=Math.abs(p[w]-y[w]),_=Math.abs(p[w]-m[w])}else x=Fp(p,y),_=Fp(p,m);Sw(Dw,p,Cw,-l*(1-(v=_/(_+x))))}bw(Tw,Tw,s),Mw(Tw,Tw,a),bw(Dw,Dw,s),Mw(Dw,Dw,a),t.bezierCurveTo(Tw[0],Tw[1],Dw[0],Dw[1],p[0],p[1]),Sw(Tw,p,Cw,l*v)}else t.lineTo(p[0],p[1]);c=d,d+=o}return f}function $l(t,e){var n=[1/0,1/0],i=[-1/0,-1/0];if(e)for(var r=0;r<t.length;r++){var o=t[r];o[0]<n[0]&&(n[0]=o[0]),o[1]<n[1]&&(n[1]=o[1]),o[0]>i[0]&&(i[0]=o[0]),o[1]>i[1]&&(i[1]=o[1])}return{min:e?n:i,max:e?i:n}}function Kl(t,e){if(t.length===e.length){for(var n=0;n<t.length;n++){var i=t[n],r=e[n];if(i[0]!==r[0]||i[1]!==r[1])return}return!0}}function Ql(t){return"number"==typeof t?t:t?.5:0}function Jl(t){var e=t.getGlobalExtent();if(t.onBand){var n=t.getBandWidth()/2-1,i=e[1]>e[0]?1:-1;e[0]+=i*n,e[1]-=i*n}return e}function tu(t,e,n){if(!n.valueDim)return[];for(var i=[],r=0,o=e.count();r<o;r++)i.push(Zl(n,t,e,r));return i}function eu(t,e,n,i){var r=Jl(t.getAxis("x")),o=Jl(t.getAxis("y")),a=t.getBaseAxis().isHorizontal(),s=Math.min(r[0],r[1]),l=Math.min(o[0],o[1]),u=Math.max(r[0],r[1])-s,h=Math.max(o[0],o[1])-l;if(n)s-=.5,u+=.5,l-=.5,h+=.5;else{var c=i.get("lineStyle.width")||2,d=i.get("clipOverflow")?c/2:Math.max(u,h);a?(l-=d,h+=2*d):(s-=d,u+=2*d)}var f=new Fv({shape:{x:s,y:l,width:u,height:h}});return e&&(f.shape[a?"width":"height"]=0,sr(f,{shape:{width:u,height:h}},i)),f}function nu(t,e,n,i){var r=t.getAngleAxis(),o=t.getRadiusAxis().getExtent().slice();o[0]>o[1]&&o.reverse();var a=r.getExtent(),s=Math.PI/180;n&&(o[0]-=.5,o[1]+=.5);var l=new zv({shape:{cx:wr(t.cx,1),cy:wr(t.cy,1),r0:wr(o[0],1),r:wr(o[1],1),startAngle:-a[0]*s,endAngle:-a[1]*s,clockwise:r.inverse}});return e&&(l.shape.endAngle=-a[0]*s,sr(l,{shape:{endAngle:-a[1]*s}},i)),l}function iu(t,e,n,i){return"polar"===t.type?nu(t,e,n,i):eu(t,e,n,i)}function ru(t,e,n){for(var i=e.getBaseAxis(),r="x"===i.dim||"radius"===i.dim?0:1,o=[],a=0;a<t.length-1;a++){var s=t[a+1],l=t[a];o.push(l);var u=[];switch(n){case"end":u[r]=s[r],u[1-r]=l[1-r],o.push(u);break;case"middle":var h=(l[r]+s[r])/2,c=[];u[r]=c[r]=h,u[1-r]=l[1-r],c[1-r]=s[1-r],o.push(u),o.push(c);break;default:u[r]=l[r],u[1-r]=s[1-r],o.push(u)}}return t[a]&&o.push(t[a]),o}function ou(t,e){var n=t.getVisual("visualMeta");if(n&&n.length&&t.count()&&"cartesian2d"===e.type){for(var i,r,o=n.length-1;o>=0;o--){var a=n[o].dimension,s=t.dimensions[a],l=t.getDimensionInfo(s);if("x"===(i=l&&l.coordDim)||"y"===i){r=n[o];break}}if(r){var u=e.getAxis(i),h=f(r.stops,function(t){return{coord:u.toGlobalCoord(u.dataToCoord(t.value)),color:t.color}}),c=h.length,p=r.outerColors.slice();c&&h[0].coord>h[c-1].coord&&(h.reverse(),p.reverse());var g=h[0].coord-10,m=h[c-1].coord+10,v=m-g;if(v<.001)return"transparent";d(h,function(t){t.offset=(t.coord-g)/v}),h.push({offset:c?h[c-1].offset:.5,color:p[1]||"transparent"}),h.unshift({offset:c?h[0].offset:.5,color:p[0]||"transparent"});var y=new jv(0,0,0,0,h,!0);return y[i]=g,y[i+"2"]=m,y}}}function au(t,e,n){var i=t.get("showAllSymbol"),r="auto"===i;if(!i||r){var o=n.getAxesByScale("ordinal")[0];if(o&&(!r||!su(o,e))){var a=e.mapDimension(o.dim),s={};return d(o.getViewLabels(),function(t){s[t.tickValue]=1}),function(t){return!s.hasOwnProperty(e.get(a,t))}}}}function su(t,e){var n=t.getExtent(),i=Math.abs(n[1]-n[0])/t.scale.count();isNaN(i)&&(i=0);for(var r=e.count(),o=Math.max(1,Math.round(r/5)),a=0;a<r;a+=o)if(1.5*El.getSymbolSize(e,a)[t.isHorizontal()?1:0]>i)return!1;return!0}function lu(t){return this._axes[t]}function uu(t){Ew.call(this,t)}function hu(t,e){return e.type||(e.data?"category":"value")}function cu(t,e,n){return t.getCoordSysModel()===e}function du(t,e,n){this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this._initCartesian(t,e,n),this.model=t}function fu(t,e,n){n.getAxesOnZeroOf=function(){return i?[i]:[]};var i,r=t[e],o=n.model,a=o.get("axisLine.onZero"),s=o.get("axisLine.onZeroAxisIndex");if(a)if(null==s){for(var l in r)if(r.hasOwnProperty(l)&&pu(r[l])){i=r[l];break}}else pu(r[s])&&(i=r[s])}function pu(t){return t&&"category"!==t.type&&"time"!==t.type&&ol(t)}function gu(t,e){var n=t.getExtent(),i=n[0]+n[1];t.toGlobalCoord="x"===t.dim?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord="x"===t.dim?function(t){return t-e}:function(t){return i-t+e}}function mu(t,e){return f(Zw,function(e){return t.getReferringComponents(e)[0]})}function vu(t){return"cartesian2d"===t.get("coordinateSystem")}function yu(t){var e={componentType:t.mainType};return e[t.mainType+"Index"]=t.componentIndex,e}function xu(t,e,n,i){var r,o,a=Tr(n-t.rotation),s=i[0]>i[1],l="start"===e&&!s||"start"!==e&&s;return Dr(a-Uw/2)?(o=l?"bottom":"top",r="center"):Dr(a-1.5*Uw)?(o=l?"top":"bottom",r="center"):(o="middle",r=a<1.5*Uw&&a>Uw/2?l?"left":"right":l?"right":"left"),{rotation:a,textAlign:r,textVerticalAlign:o}}function _u(t){var e=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||e&&e.show)}function wu(t,e,n){var i=t.get("axisLabel.showMinLabel"),r=t.get("axisLabel.showMaxLabel");e=e||[],n=n||[];var o=e[0],a=e[1],s=e[e.length-1],l=e[e.length-2],u=n[0],h=n[1],c=n[n.length-1],d=n[n.length-2];!1===i?(bu(o),bu(u)):Mu(o,a)&&(i?(bu(a),bu(h)):(bu(o),bu(u))),!1===r?(bu(s),bu(c)):Mu(l,s)&&(r?(bu(l),bu(d)):(bu(s),bu(c)))}function bu(t){t&&(t.ignore=!0)}function Mu(t,e,n){var i=t&&t.getBoundingRect().clone(),r=e&&e.getBoundingRect().clone();if(i&&r){var o=ot([]);return ut(o,o,-t.rotation),i.applyTransform(st([],o,t.getLocalTransform())),r.applyTransform(st([],o,e.getLocalTransform())),i.intersect(r)}}function Su(t){return"middle"===t||"center"===t}function Iu(t,e,n){var i=e.axis;if(e.get("axisTick.show")&&!i.scale.isBlank()){for(var r=e.getModel("axisTick"),o=r.getModel("lineStyle"),s=r.get("length"),l=i.getTicksCoords(),u=[],h=[],c=t._transform,d=[],f=0;f<l.length;f++){var p=l[f].coord;u[0]=p,u[1]=0,h[0]=p,h[1]=n.tickDirection*s,c&&($(u,u,c),$(h,h,c));var g=new Hv(zi({anid:"tick_"+l[f].tickValue,shape:{x1:u[0],y1:u[1],x2:h[0],y2:h[1]},style:a(o.getLineStyle(),{stroke:e.get("axisLine.lineStyle.color")}),z2:2,silent:!0}));t.group.add(g),d.push(g)}return d}}function Cu(t,e,n){var i=e.axis;if(C(n.axisLabelShow,e.get("axisLabel.show"))&&!i.scale.isBlank()){var r=e.getModel("axisLabel"),o=r.get("margin"),a=i.getViewLabels(),s=(C(n.labelRotate,r.get("rotate"))||0)*Uw/180,l=Yw(n.rotation,s,n.labelDirection),u=e.getCategories(!0),h=[],c=_u(e),f=e.get("triggerEvent");return d(a,function(a,s){var d=a.tickValue,p=a.formattedLabel,g=a.rawLabel,m=r;u&&u[d]&&u[d].textStyle&&(m=new pr(u[d].textStyle,r,e.ecModel));var v=m.getTextColor()||e.get("axisLine.lineStyle.color"),y=[i.dataToCoord(d),n.labelOffset+n.labelDirection*o],x=new kv({anid:"label_"+d,position:y,rotation:l.rotation,silent:c,z2:10});Ki(x.style,m,{text:p,textAlign:m.getShallow("align",!0)||l.textAlign,textVerticalAlign:m.getShallow("verticalAlign",!0)||m.getShallow("baseline",!0)||l.textVerticalAlign,textFill:"function"==typeof v?v("category"===i.type?g:"value"===i.type?d+"":d,s):v}),f&&(x.eventData=yu(e),x.eventData.targetType="axisLabel",x.eventData.value=g),t._dumbGroup.add(x),x.updateTransform(),h.push(x),t.group.add(x),x.decomposeTransform()}),h}}function Tu(t,e){var n={axesInfo:{},seriesInvolved:!1,coordSysAxesInfo:{},coordSysMap:{}};return Du(n,t,e),n.seriesInvolved&&ku(n,t),n}function Du(t,e,n){var i=e.getComponent("tooltip"),r=e.getComponent("axisPointer"),o=r.get("link",!0)||[],a=[];qw(n.getCoordinateSystems(),function(n){function s(i,s,l){var c=l.model.getModel("axisPointer",r),d=c.get("show");if(d&&("auto"!==d||i||Nu(c))){null==s&&(s=c.get("triggerTooltip"));var f=(c=i?Au(l,h,r,e,i,s):c).get("snap"),p=Ru(l.model),g=s||f||"category"===l.type,m=t.axesInfo[p]={key:p,axis:l,coordSys:n,axisPointerModel:c,triggerTooltip:s,involveSeries:g,snap:f,useHandle:Nu(c),seriesModels:[]};u[p]=m,t.seriesInvolved|=g;var v=Pu(o,l);if(null!=v){var y=a[v]||(a[v]={axesInfo:{}});y.axesInfo[p]=m,y.mapper=o[v].mapper,m.linkGroup=y}}}if(n.axisPointerEnabled){var l=Ru(n.model),u=t.coordSysAxesInfo[l]={};t.coordSysMap[l]=n;var h=n.model.getModel("tooltip",i);if(qw(n.getAxes(),$w(s,!1,null)),n.getTooltipAxes&&i&&h.get("show")){var c="axis"===h.get("trigger"),d="cross"===h.get("axisPointer.type"),f=n.getTooltipAxes(h.get("axisPointer.axis"));(c||d)&&qw(f.baseAxes,$w(s,!d||"cross",c)),d&&qw(f.otherAxes,$w(s,"cross",!1))}}})}function Au(t,e,i,r,o,s){var l=e.getModel("axisPointer"),u={};qw(["type","snap","lineStyle","shadowStyle","label","animation","animationDurationUpdate","animationEasingUpdate","z"],function(t){u[t]=n(l.get(t))}),u.snap="category"!==t.type&&!!s,"cross"===l.get("type")&&(u.type="line");var h=u.label||(u.label={});if(null==h.show&&(h.show=!1),"cross"===o){var c=l.get("label.show");if(h.show=null==c||c,!s){var d=u.lineStyle=l.get("crossStyle");d&&a(h,d.textStyle)}}return t.model.getModel("axisPointer",new pr(u,i,r))}function ku(t,e){e.eachSeries(function(e){var n=e.coordinateSystem,i=e.get("tooltip.trigger",!0),r=e.get("tooltip.show",!0);n&&"none"!==i&&!1!==i&&"item"!==i&&!1!==r&&!1!==e.get("axisPointer.show",!0)&&qw(t.coordSysAxesInfo[Ru(n.model)],function(t){var i=t.axis;n.getAxis(i.dim)===i&&(t.seriesModels.push(e),null==t.seriesDataCount&&(t.seriesDataCount=0),t.seriesDataCount+=e.getData().count())})},this)}function Pu(t,e){for(var n=e.model,i=e.dim,r=0;r<t.length;r++){var o=t[r]||{};if(Lu(o[i+"AxisId"],n.id)||Lu(o[i+"AxisIndex"],n.componentIndex)||Lu(o[i+"AxisName"],n.name))return r}}function Lu(t,e){return"all"===t||y(t)&&l(t,e)>=0||t===e}function Ou(t){var e=zu(t);if(e){var n=e.axisPointerModel,i=e.axis.scale,r=n.option,o=n.get("status"),a=n.get("value");null!=a&&(a=i.parse(a));var s=Nu(n);null==o&&(r.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==a||a>l[1])&&(a=l[1]),a<l[0]&&(a=l[0]),r.value=a,s&&(r.status=e.axis.scale.isBlank()?"hide":"show")}}function zu(t){var e=(t.ecModel.getComponent("axisPointer")||{}).coordSysAxesInfo;return e&&e.axesInfo[Ru(t)]}function Eu(t){var e=zu(t);return e&&e.axisPointerModel}function Nu(t){return!!t.get("handle.show")}function Ru(t){return t.type+"||"+t.id}function Bu(t,e,n,i,r,o){var a=Kw.getAxisPointerClass(t.axisPointerClass);if(a){var s=Eu(e);s?(t._axisPointer||(t._axisPointer=new a)).render(e,s,i,o):Vu(t,i)}}function Vu(t,e,n){var i=t._axisPointer;i&&i.dispose(e,n),t._axisPointer=null}function Fu(t,e,n){n=n||{};var i=t.coordinateSystem,r=e.axis,o={},a=r.getAxesOnZeroOf()[0],s=r.position,l=a?"onZero":s,u=r.dim,h=i.getRect(),c=[h.x,h.x+h.width,h.y,h.y+h.height],d={left:0,right:1,top:0,bottom:1,onZero:2},f=e.get("offset")||0,p="x"===u?[c[2]-f,c[3]+f]:[c[0]-f,c[1]+f];if(a){var g=a.toGlobalCoord(a.dataToCoord(0));p[d.onZero]=Math.max(Math.min(g,p[1]),p[0])}o.position=["y"===u?p[d[l]]:c[0],"x"===u?p[d[l]]:c[3]],o.rotation=Math.PI/2*("x"===u?0:1);var m={top:-1,bottom:1,left:-1,right:1};o.labelDirection=o.tickDirection=o.nameDirection=m[s],o.labelOffset=a?p[d[s]]-p[d.onZero]:0,e.get("axisTick.inside")&&(o.tickDirection=-o.tickDirection),C(n.labelInside,e.get("axisLabel.inside"))&&(o.labelDirection=-o.labelDirection);var v=e.get("axisLabel.rotate");return o.labelRotate="top"===l?-v:v,o.z2=1,o}function Hu(t,e,n,i,r,o,a){$i(t,e,n.getModel("label"),n.getModel("emphasis.label"),{labelFetcher:r,labelDataIndex:o,defaultText:zl(r.getData(),o),isRectText:!0,autoColor:i}),Gu(t),Gu(e)}function Gu(t,e){"outside"===t.textPosition&&(t.textPosition=e)}function Wu(t,e,n){n.style.text=null,ar(n,{shape:{width:0}},e,t,function(){n.parent&&n.parent.remove(n)})}function Zu(t,e,n){n.style.text=null,ar(n,{shape:{r:n.shape.r0}},e,t,function(){n.parent&&n.parent.remove(n)})}function Uu(t,e,n,i,r,o,s,l){var u=e.getItemVisual(n,"color"),h=e.getItemVisual(n,"opacity"),c=i.getModel("itemStyle"),d=i.getModel("emphasis.itemStyle").getBarItemStyle();l||t.setShape("r",c.get("barBorderRadius")||0),t.useStyle(a({fill:u,opacity:h},c.getBarItemStyle()));var f=i.getShallow("cursor");f&&t.attr("cursor",f);var p=s?r.height>0?"bottom":"top":r.width>0?"left":"right";l||Hu(t.style,d,i,u,o,n,p),qi(t,d)}function Xu(t,e){var n=t.get(rb)||0;return Math.min(n,Math.abs(e.width),Math.abs(e.height))}function ju(t,e,n){var i=t.getData(),r=[],o=i.getLayout("valueAxisHorizontal")?1:0;r[1-o]=i.getLayout("valueAxisStart");var a=new sb({shape:{points:i.getLayout("largePoints")},incremental:!!n,__startPoint:r,__valueIdx:o});e.add(a),Yu(a,t,i)}function Yu(t,e,n){var i=n.getVisual("borderColor")||n.getVisual("color"),r=e.getModel("itemStyle").getItemStyle(["color","borderColor"]);t.useStyle(r),t.style.fill=null,t.style.stroke=i,t.style.lineWidth=n.getLayout("barWidth")}function qu(t,e,n,i){var r=e.getData(),o=this.dataIndex,a=r.getName(o),s=e.get("selectedOffset");i.dispatchAction({type:"pieToggleSelect",from:t,name:a,seriesId:e.id}),r.each(function(t){$u(r.getItemGraphicEl(t),r.getItemLayout(t),e.isSelected(r.getName(t)),s,n)})}function $u(t,e,n,i,r){var o=(e.startAngle+e.endAngle)/2,a=Math.cos(o),s=Math.sin(o),l=n?i:0,u=[a*l,s*l];r?t.animate().when(200,{position:u}).start("bounceOut"):t.attr("position",u)}function Ku(t,e){function n(){o.ignore=o.hoverIgnore,a.ignore=a.hoverIgnore}function i(){o.ignore=o.normalIgnore,a.ignore=a.normalIgnore}Sg.call(this);var r=new zv({z2:2}),o=new Vv,a=new kv;this.add(r),this.add(o),this.add(a),this.updateData(t,e,!0),this.on("emphasis",n).on("normal",i).on("mouseover",n).on("mouseout",i)}function Qu(t,e,n,i,r,o,a){function s(e,n){for(var i=e;i>=0&&(t[i].y-=n,!(i>0&&t[i].y>t[i-1].y+t[i-1].height));i--);}function l(t,e,n,i,r,o){for(var a=e?Number.MAX_VALUE:0,s=0,l=t.length;s<l;s++)if("center"!==t[s].position){var u=Math.abs(t[s].y-i),h=t[s].len,c=t[s].len2,d=u<r+h?Math.sqrt((r+h+c)*(r+h+c)-u*u):Math.abs(t[s].x-n);e&&d>=a&&(d=a-10),!e&&d<=a&&(d=a+10),t[s].x=n+d*o,a=d}}t.sort(function(t,e){return t.y-e.y});for(var u,h=0,c=t.length,d=[],f=[],p=0;p<c;p++)(u=t[p].y-h)<0&&function(e,n,i,r){for(var o=e;o<n;o++)if(t[o].y+=i,o>e&&o+1<n&&t[o+1].y>t[o].y+t[o].height)return void s(o,i/2);s(n-1,i/2)}(p,c,-u),h=t[p].y+t[p].height;a-h<0&&s(c-1,h-a);for(p=0;p<c;p++)t[p].y>=n?f.push(t[p]):d.push(t[p]);l(d,!1,e,n,i,r),l(f,!0,e,n,i,r)}function Ju(t,e,n,i,r,o){for(var a=[],s=[],l=0;l<t.length;l++)t[l].x<e?a.push(t[l]):s.push(t[l]);Qu(s,e,n,i,1,r,o),Qu(a,e,n,i,-1,r,o);for(l=0;l<t.length;l++){var u=t[l].linePoints;if(u){var h=u[1][0]-u[2][0];t[l].x<e?u[2][0]=t[l].x+3:u[2][0]=t[l].x-3,u[1][1]=u[2][1]=t[l].y,u[1][0]=u[2][0]+h}}}function th(){this.group=new Sg}function eh(t,e,n,i){var r=n.type,o=new(0,ty[r.charAt(0).toUpperCase()+r.slice(1)])(n);e.add(o),i.set(t,o),o.__ecGraphicId=t}function nh(t,e){var n=t&&t.parent;n&&("group"===t.type&&t.traverse(function(t){nh(t,e)}),e.removeKey(t.__ecGraphicId),n.remove(t))}function ih(t){return t=o({},t),d(["id","parentId","$action","hv","bounding"].concat(_y),function(e){delete t[e]}),t}function rh(t,e){var n;return d(e,function(e){null!=t[e]&&"auto"!==t[e]&&(n=!0)}),n}function oh(t,e){var n=t.exist;if(e.id=t.keyInfo.id,!e.type&&n&&(e.type=n.type),null==e.parentId){var i=e.parentOption;i?e.parentId=i.id:n&&(e.parentId=n.parentId)}e.parentOption=null}function ah(t,e,n){var r=o({},n),a=t[e],s=n.$action||"merge";"merge"===s?a?(i(a,r,!0),Zr(a,r,{ignoreSize:!0}),Xr(n,a)):t[e]=r:"replace"===s?t[e]=r:"remove"===s&&a&&(t[e]=null)}function sh(t,e){t&&(t.hv=e.hv=[rh(e,["left","right"]),rh(e,["top","bottom"])],"group"===t.type&&(null==t.width&&(t.width=e.width=0),null==t.height&&(t.height=e.height=0)))}function lh(t,e,n,i,r){var a=t.axis;if(!a.scale.isBlank()&&a.containData(e))if(t.involveSeries){var s=uh(e,t),l=s.payloadBatch,u=s.snapToValue;l[0]&&null==r.seriesIndex&&o(r,l[0]),!i&&t.snap&&a.containData(u)&&null!=u&&(e=u),n.showPointer(t,e,l,r),n.showTooltip(t,s,u)}else n.showPointer(t,e)}function uh(t,e){var n=e.axis,i=n.dim,r=t,o=[],a=Number.MAX_VALUE,s=-1;return xb(e.seriesModels,function(e,l){var u,h,c=e.getData().mapDimension(i,!0);if(e.getAxisTooltipData){var d=e.getAxisTooltipData(c,t,n);h=d.dataIndices,u=d.nestestValue}else{if(!(h=e.getData().indicesOfNearest(c[0],t,"category"===n.type?.5:null)).length)return;u=e.getData().get(c[0],h[0])}if(null!=u&&isFinite(u)){var f=t-u,p=Math.abs(f);p<=a&&((p<a||f>=0&&s<0)&&(a=p,s=f,r=u,o.length=0),xb(h,function(t){o.push({seriesIndex:e.seriesIndex,dataIndexInside:t,dataIndex:e.getData().getRawIndex(t)})}))}}),{payloadBatch:o,snapToValue:r}}function hh(t,e,n,i){t[e.key]={value:n,payloadBatch:i}}function ch(t,e,n,i){var r=n.payloadBatch,o=e.axis,a=o.model,s=e.axisPointerModel;if(e.triggerTooltip&&r.length){var l=e.coordSys.model,u=Ru(l),h=t.map[u];h||(h=t.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},t.list.push(h)),h.dataByAxis.push({axisDim:o.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:i,valueLabelOpt:{precision:s.get("label.precision"),formatter:s.get("label.formatter")},seriesDataIndices:r.slice()})}}function dh(t,e,n){var i=n.axesInfo=[];xb(e,function(e,n){var r=e.axisPointerModel.option,o=t[n];o?(!e.useHandle&&(r.status="show"),r.value=o.value,r.seriesDataIndices=(o.payloadBatch||[]).slice()):!e.useHandle&&(r.status="hide"),"show"===r.status&&i.push({axisDim:e.axis.dim,axisIndex:e.axis.model.componentIndex,value:r.value})})}function fh(t,e,n,i){if(!vh(e)&&t.list.length){var r=((t.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:t.list})}else i({type:"hideTip"})}function ph(t,e,n){var i=n.getZr(),r=wb(i).axisPointerLastHighlights||{},o=wb(i).axisPointerLastHighlights={};xb(t,function(t,e){var n=t.axisPointerModel.option;"show"===n.status&&xb(n.seriesDataIndices,function(t){var e=t.seriesIndex+" | "+t.dataIndex;o[e]=t})});var a=[],s=[];d(r,function(t,e){!o[e]&&s.push(t)}),d(o,function(t,e){!r[e]&&a.push(t)}),s.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,batch:s}),a.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,batch:a})}function gh(t,e){for(var n=0;n<(t||[]).length;n++){var i=t[n];if(e.axis.dim===i.axisDim&&e.axis.model.componentIndex===i.axisIndex)return i}}function mh(t){var e=t.axis.model,n={},i=n.axisDim=t.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=e.componentIndex,n.axisName=n[i+"AxisName"]=e.name,n.axisId=n[i+"AxisId"]=e.id,n}function vh(t){return!t||null==t[0]||isNaN(t[0])||null==t[1]||isNaN(t[1])}function yh(t,e,n){if(!bp.node){var i=e.getZr();bb(i).records||(bb(i).records={}),xh(i,e),(bb(i).records[t]||(bb(i).records[t]={})).handler=n}}function xh(t,e){function n(n,i){t.on(n,function(n){var r=Mh(e);Mb(bb(t).records,function(t){t&&i(t,n,r.dispatchAction)}),_h(r.pendings,e)})}bb(t).initialized||(bb(t).initialized=!0,n("click",v(bh,"click")),n("mousemove",v(bh,"mousemove")),n("globalout",wh))}function _h(t,e){var n,i=t.showTip.length,r=t.hideTip.length;i?n=t.showTip[i-1]:r&&(n=t.hideTip[r-1]),n&&(n.dispatchAction=null,e.dispatchAction(n))}function wh(t,e,n){t.handler("leave",null,n)}function bh(t,e,n,i){e.handler(t,n,i)}function Mh(t){var e={showTip:[],hideTip:[]},n=function(i){var r=e[i.type];r?r.push(i):(i.dispatchAction=n,t.dispatchAction(i))};return{dispatchAction:n,pendings:e}}function Sh(t,e){if(!bp.node){var n=e.getZr();(bb(n).records||{})[t]&&(bb(n).records[t]=null)}}function Ih(){}function Ch(t,e,n,i){Th(Ib(n).lastProp,i)||(Ib(n).lastProp=i,e?ar(n,i,t):(n.stopAnimation(),n.attr(i)))}function Th(t,e){if(w(t)&&w(e)){var n=!0;return d(e,function(e,i){n=n&&Th(t[i],e)}),!!n}return t===e}function Dh(t,e){t[e.get("label.show")?"show":"hide"]()}function Ah(t){return{position:t.position.slice(),rotation:t.rotation||0}}function kh(t,e,n){var i=e.get("z"),r=e.get("zlevel");t&&t.traverse(function(t){"group"!==t.type&&(null!=i&&(t.z=i),null!=r&&(t.zlevel=r),t.silent=n)})}function Ph(t){var e,n=t.get("type"),i=t.getModel(n+"Style");return"line"===n?(e=i.getLineStyle()).fill=null:"shadow"===n&&((e=i.getAreaStyle()).stroke=null),e}function Lh(t,e,n,i,r){var o=zh(n.get("value"),e.axis,e.ecModel,n.get("seriesDataIndices"),{precision:n.get("label.precision"),formatter:n.get("label.formatter")}),a=n.getModel("label"),s=cy(a.get("padding")||0),l=a.getFont(),u=ce(o,l),h=r.position,c=u.width+s[1]+s[3],d=u.height+s[0]+s[2],f=r.align;"right"===f&&(h[0]-=c),"center"===f&&(h[0]-=c/2);var p=r.verticalAlign;"bottom"===p&&(h[1]-=d),"middle"===p&&(h[1]-=d/2),Oh(h,c,d,i);var g=a.get("backgroundColor");g&&"auto"!==g||(g=e.get("axisLine.lineStyle.color")),t.label={shape:{x:0,y:0,width:c,height:d,r:a.get("borderRadius")},position:h.slice(),style:{text:o,textFont:l,textFill:a.getTextColor(),textPosition:"inside",fill:g,stroke:a.get("borderColor")||"transparent",lineWidth:a.get("borderWidth")||0,shadowBlur:a.get("shadowBlur"),shadowColor:a.get("shadowColor"),shadowOffsetX:a.get("shadowOffsetX"),shadowOffsetY:a.get("shadowOffsetY")},z2:10}}function Oh(t,e,n,i){var r=i.getWidth(),o=i.getHeight();t[0]=Math.min(t[0]+e,r)-e,t[1]=Math.min(t[1]+n,o)-n,t[0]=Math.max(t[0],0),t[1]=Math.max(t[1],0)}function zh(t,e,n,i,r){t=e.scale.parse(t);var o=e.scale.getLabel(t,{precision:r.precision}),a=r.formatter;if(a){var s={value:sl(e,t),seriesData:[]};d(i,function(t){var e=n.getSeriesByIndex(t.seriesIndex),i=t.dataIndexInside,r=e&&e.getDataParams(i);r&&s.seriesData.push(r)}),_(a)?o=a.replace("{value}",o):x(a)&&(o=a(s))}return o}function Eh(t,e,n){var i=rt();return ut(i,i,n.rotation),lt(i,i,n.position),ur([t.dataToCoord(e),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function Nh(t,e,n,i,r,o){var a=Xw.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get("label.margin"),Lh(e,i,r,o,{position:Eh(i.axis,t,n),align:a.textAlign,verticalAlign:a.textVerticalAlign})}function Rh(t,e,n){return n=n||0,{x1:t[n],y1:t[1-n],x2:e[n],y2:e[1-n]}}function Bh(t,e,n){return n=n||0,{x:t[n],y:t[1-n],width:e[n],height:e[1-n]}}function Vh(t,e){var n={};return n[e.dim+"AxisIndex"]=e.index,t.getCartesian(n)}function Fh(t){return"x"===t.dim?0:1}function Hh(t){var e="left "+t+"s cubic-bezier(0.23, 1, 0.32, 1),top "+t+"s cubic-bezier(0.23, 1, 0.32, 1)";return f(Lb,function(t){return t+"transition:"+e}).join(";")}function Gh(t){var e=[],n=t.get("fontSize"),i=t.getTextColor();return i&&e.push("color:"+i),e.push("font:"+t.getFont()),n&&e.push("line-height:"+Math.round(3*n/2)+"px"),kb(["decoration","align"],function(n){var i=t.get(n);i&&e.push("text-"+n+":"+i)}),e.join(";")}function Wh(t){var e=[],n=t.get("transitionDuration"),i=t.get("backgroundColor"),r=t.getModel("textStyle"),o=t.get("padding");return n&&e.push(Hh(n)),i&&(bp.canvasSupported?e.push("background-Color:"+i):(e.push("background-Color:#"+Dt(i)),e.push("filter:alpha(opacity=70)"))),kb(["width","color","radius"],function(n){var i="border-"+n,r=Pb(i),o=t.get(r);null!=o&&e.push(i+":"+o+("color"===n?"":"px"))}),e.push(Gh(r)),null!=o&&e.push("padding:"+cy(o).join("px ")+"px"),e.join(";")+";"}function Zh(t,e){if(bp.wxa)return null;var n=document.createElement("div"),i=this._zr=e.getZr();this.el=n,this._x=e.getWidth()/2,this._y=e.getHeight()/2,t.appendChild(n),this._container=t,this._show=!1,this._hideTimeout;var r=this;n.onmouseenter=function(){r._enterable&&(clearTimeout(r._hideTimeout),r._show=!0),r._inContent=!0},n.onmousemove=function(e){if(e=e||window.event,!r._enterable){var n=i.handler;rn(t,e,!0),n.dispatch("mousemove",e)}},n.onmouseleave=function(){r._enterable&&r._show&&r.hideLater(r._hideDelay),r._inContent=!1}}function Uh(t){for(var e=t.pop();t.length;){var n=t.pop();n&&(pr.isInstance(n)&&(n=n.get("tooltip",!0)),"string"==typeof n&&(n={formatter:n}),e=new pr(n,e,e.ecModel))}return e}function Xh(t,e){return t.dispatchAction||m(e.dispatchAction,e)}function jh(t,e,n,i,r,o,a){var s=qh(n),l=s.width,u=s.height;return null!=o&&(t+l+o>i?t-=l+o:t+=o),null!=a&&(e+u+a>r?e-=u+a:e+=a),[t,e]}function Yh(t,e,n,i,r){var o=qh(n),a=o.width,s=o.height;return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}function qh(t){var e=t.clientWidth,n=t.clientHeight;if(document.defaultView&&document.defaultView.getComputedStyle){var i=document.defaultView.getComputedStyle(t);i&&(e+=parseInt(i.paddingLeft,10)+parseInt(i.paddingRight,10)+parseInt(i.borderLeftWidth,10)+parseInt(i.borderRightWidth,10),n+=parseInt(i.paddingTop,10)+parseInt(i.paddingBottom,10)+parseInt(i.borderTopWidth,10)+parseInt(i.borderBottomWidth,10))}return{width:e,height:n}}function $h(t,e,n){var i=n[0],r=n[1],o=0,a=0,s=e.width,l=e.height;switch(t){case"inside":o=e.x+s/2-i/2,a=e.y+l/2-r/2;break;case"top":o=e.x+s/2-i/2,a=e.y-r-5;break;case"bottom":o=e.x+s/2-i/2,a=e.y+l+5;break;case"left":o=e.x-i-5,a=e.y+l/2-r/2;break;case"right":o=e.x+s+5,a=e.y+l/2-r/2}return[o,a]}function Kh(t){return"center"===t||"middle"===t}function Qh(t,e,n){var i,r={},o="toggleSelected"===t;return n.eachComponent("legend",function(n){o&&null!=i?n[i?"select":"unSelect"](e.name):(n[t](e.name),i=n.isSelected(e.name)),d(n.getData(),function(t){var e=t.get("name");if("\n"!==e&&""!==e){var i=n.isSelected(e);r.hasOwnProperty(e)?r[e]=r[e]&&i:r[e]=i}})}),{name:e.name,selected:r}}function Jh(t,e,n){var i=e.getBoxLayoutParams(),r=e.get("padding"),o={width:n.getWidth(),height:n.getHeight()},a=Gr(i,o,r);by(e.get("orient"),t,e.get("itemGap"),a.width,a.height),Wr(t,i,o,r)}function tc(t,e){var n=cy(e.get("padding")),i=e.getItemStyle(["color","opacity"]);return i.fill=e.get("backgroundColor"),t=new Fv({shape:{x:t.x-n[3],y:t.y-n[0],width:t.width+n[1]+n[3],height:t.height+n[0]+n[2],r:e.get("borderRadius")},style:i,silent:!0,z2:-1})}function ec(t,e){e.dispatchAction({type:"legendToggleSelect",name:t})}function nc(t,e,n,i){var r=n.getZr().storage.getDisplayList()[0];r&&r.useHoverLayer||n.dispatchAction({type:"highlight",seriesName:t.name,name:e,excludeSeriesId:i})}function ic(t,e,n,i){var r=n.getZr().storage.getDisplayList()[0];r&&r.useHoverLayer||n.dispatchAction({type:"downplay",seriesName:t.name,name:e,excludeSeriesId:i})}function rc(t,e,n){var i=[1,1];i[t.getOrient().index]=0,Zr(e,n,{type:"box",ignoreSize:i})}function oc(t){_n(t,"label",["show"])}function ac(t){return!(isNaN(parseFloat(t.x))&&isNaN(parseFloat(t.y)))}function sc(t){return!isNaN(parseFloat(t.x))&&!isNaN(parseFloat(t.y))}function lc(t,e,n,i,r,o){var a=[],s=Ps(e,i)?e.getCalculationInfo("stackResultDimension"):i,l=pc(e,s,t),u=e.indicesOfNearest(s,l)[0];a[r]=e.get(n,u),a[o]=e.get(i,u);var h=Mr(e.get(i,u));return(h=Math.min(h,20))>=0&&(a[o]=+a[o].toFixed(h)),a}function uc(t,e){var i=t.getData(),r=t.coordinateSystem;if(e&&!sc(e)&&!y(e.coord)&&r){var o=r.dimensions,a=hc(e,i,r,t);if((e=n(e)).type&&Qb[e.type]&&a.baseAxis&&a.valueAxis){var s=$b(o,a.baseAxis.dim),l=$b(o,a.valueAxis.dim);e.coord=Qb[e.type](i,a.baseDataDim,a.valueDataDim,s,l),e.value=e.coord[l]}else{for(var u=[null!=e.xAxis?e.xAxis:e.radiusAxis,null!=e.yAxis?e.yAxis:e.angleAxis],h=0;h<2;h++)Qb[u[h]]&&(u[h]=pc(i,i.mapDimension(o[h]),u[h]));e.coord=u}}return e}function hc(t,e,n,i){var r={};return null!=t.valueIndex||null!=t.valueDim?(r.valueDataDim=null!=t.valueIndex?e.getDimension(t.valueIndex):t.valueDim,r.valueAxis=n.getAxis(cc(i,r.valueDataDim)),r.baseAxis=n.getOtherAxis(r.valueAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim)):(r.baseAxis=i.getBaseAxis(),r.valueAxis=n.getOtherAxis(r.baseAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim),r.valueDataDim=e.mapDimension(r.valueAxis.dim)),r}function cc(t,e){var n=t.getData(),i=n.dimensions;e=n.getDimension(e);for(var r=0;r<i.length;r++){var o=n.getDimensionInfo(i[r]);if(o.name===e)return o.coordDim}}function dc(t,e){return!(t&&t.containData&&e.coord&&!ac(e))||t.containData(e.coord)}function fc(t,e,n,i){return i<2?t.coord&&t.coord[i]:t.value}function pc(t,e,n){if("average"===n){var i=0,r=0;return t.each(e,function(t,e){isNaN(t)||(i+=t,r++)}),i/r}return"median"===n?t.getMedian(e):t.getDataExtent(e,!0)["max"===n?1:0]}function gc(t,e,n){var i=e.coordinateSystem;t.each(function(r){var o,a=t.getItemModel(r),s=_r(a.get("x"),n.getWidth()),l=_r(a.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(e.getMarkerPosition)o=e.getMarkerPosition(t.getValues(t.dimensions,r));else if(i){var u=t.get(i.dimensions[0],r),h=t.get(i.dimensions[1],r);o=i.dataToPoint([u,h])}}else o=[s,l];isNaN(s)||(o[0]=s),isNaN(l)||(o[1]=l),t.setItemLayout(r,o)})}function mc(t,e,n){var i;i=t?f(t&&t.dimensions,function(t){return a({name:t},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{})}):[{name:"value",type:"float"}];var r=new M_(i,n),o=f(n.get("data"),v(uc,e));return t&&(o=g(o,v(dc,t))),r.initData(o,null,t?fc:function(t){return t.value}),r}function vc(t){return isNaN(+t.cpx1)||isNaN(+t.cpy1)}function yc(t){return"_"+t+"Type"}function xc(t,e,n){var i=e.getItemVisual(n,"color"),r=e.getItemVisual(n,t),o=e.getItemVisual(n,t+"Size");if(r&&"none"!==r){y(o)||(o=[o,o]);var a=cl(r,-o[0]/2,-o[1]/2,o[0],o[1],i);return a.name=t,a}}function _c(t){var e=new nM({name:"line"});return wc(e.shape,t),e}function wc(t,e){var n=e[0],i=e[1],r=e[2];t.x1=n[0],t.y1=n[1],t.x2=i[0],t.y2=i[1],t.percent=1,r?(t.cpx1=r[0],t.cpy1=r[1]):(t.cpx1=NaN,t.cpy1=NaN)}function bc(t,e,n){Sg.call(this),this._createLine(t,e,n)}function Mc(t){this._ctor=t||bc,this.group=new Sg}function Sc(t,e,n,i){if(Dc(e.getItemLayout(n))){var r=new t._ctor(e,n,i);e.setItemGraphicEl(n,r),t.group.add(r)}}function Ic(t,e,n,i,r,o){var a=e.getItemGraphicEl(i);Dc(n.getItemLayout(r))?(a?a.updateData(n,r,o):a=new t._ctor(n,r,o),n.setItemGraphicEl(r,a),t.group.add(a)):t.group.remove(a)}function Cc(t){var e=t.hostModel;return{lineStyle:e.getModel("lineStyle").getLineStyle(),hoverLineStyle:e.getModel("emphasis.lineStyle").getLineStyle(),labelModel:e.getModel("label"),hoverLabelModel:e.getModel("emphasis.label")}}function Tc(t){return isNaN(t[0])||isNaN(t[1])}function Dc(t){return!Tc(t[0])&&!Tc(t[1])}function Ac(t){return!isNaN(t)&&!isFinite(t)}function kc(t,e,n,i){var r=1-t,o=i.dimensions[t];return Ac(e[r])&&Ac(n[r])&&e[t]===n[t]&&i.getAxis(o).containData(e[t])}function Pc(t,e){if("cartesian2d"===t.type){var n=e[0].coord,i=e[1].coord;if(n&&i&&(kc(1,n,i,t)||kc(0,n,i,t)))return!0}return dc(t,e[0])&&dc(t,e[1])}function Lc(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=_r(s.get("x"),r.getWidth()),u=_r(s.get("y"),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(t.dimensions,e));else{var h=a.dimensions,c=t.get(h[0],e),d=t.get(h[1],e);o=a.dataToPoint([c,d])}if("cartesian2d"===a.type){var f=a.getAxis("x"),p=a.getAxis("y"),h=a.dimensions;Ac(t.get(h[0],e))?o[0]=f.toGlobalCoord(f.getExtent()[n?0:1]):Ac(t.get(h[1],e))&&(o[1]=p.toGlobalCoord(p.getExtent()[n?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];t.setItemLayout(e,o)}function Oc(t,e,n){var i;i=t?f(t&&t.dimensions,function(t){return a({name:t},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{})}):[{name:"value",type:"float"}];var r=new M_(i,n),o=new M_(i,n),s=new M_([],n),l=f(n.get("data"),v(aM,e,t,n));t&&(l=g(l,v(Pc,t)));var u=t?fc:function(t){return t.value};return r.initData(f(l,function(t){return t[0]}),null,u),o.initData(f(l,function(t){return t[1]}),null,u),s.initData(f(l,function(t){return t[2]})),s.hasItemOption=!0,{from:r,to:o,line:s}}function zc(t){return!isNaN(t)&&!isFinite(t)}function Ec(t,e,n,i){var r=1-t;return zc(e[r])&&zc(n[r])}function Nc(t,e){var n=e.coord[0],i=e.coord[1];return!("cartesian2d"!==t.type||!n||!i||!Ec(1,n,i,t)&&!Ec(0,n,i,t))||(dc(t,{coord:n,x:e.x0,y:e.y0})||dc(t,{coord:i,x:e.x1,y:e.y1}))}function Rc(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=_r(s.get(n[0]),r.getWidth()),u=_r(s.get(n[1]),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(n,e));else{var h=[f=t.get(n[0],e),p=t.get(n[1],e)];a.clampData&&a.clampData(h,h),o=a.dataToPoint(h,!0)}if("cartesian2d"===a.type){var c=a.getAxis("x"),d=a.getAxis("y"),f=t.get(n[0],e),p=t.get(n[1],e);zc(f)?o[0]=c.toGlobalCoord(c.getExtent()["x0"===n[0]?0:1]):zc(p)&&(o[1]=d.toGlobalCoord(d.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];return o}function Bc(t,e,n){var i,r,o=["x0","y0","x1","y1"];t?(i=f(t&&t.dimensions,function(t){var n=e.getData();return a({name:t},n.getDimensionInfo(n.mapDimension(t))||{})}),r=new M_(f(o,function(t,e){return{name:t,type:i[e%2].type}}),n)):r=new M_(i=[{name:"value",type:"float"}],n);var s=f(n.get("data"),v(sM,e,t,n));t&&(s=g(s,v(Nc,t)));var l=t?function(t,e,n,i){return t.coord[Math.floor(i/2)][i%2]}:function(t){return t.value};return r.initData(s,null,l),r.hasItemOption=!0,r}function Vc(t){return l(uM,t)>=0}function Fc(t,e,n){function i(t,e){return l(e.nodes,t)>=0}function r(t,i){var r=!1;return e(function(e){d(n(t,e)||[],function(t){i.records[e.name][t]&&(r=!0)})}),r}function o(t,i){i.nodes.push(t),e(function(e){d(n(t,e)||[],function(t){i.records[e.name][t]=!0})})}return function(n){var a={nodes:[],records:{}};if(e(function(t){a.records[t.name]={}}),!n)return a;o(n,a);var s;do{s=!1,t(function(t){!i(t,a)&&r(t,a)&&(o(t,a),s=!0)})}while(s);return a}}function Hc(t,e,n){var i=[1/0,-1/0];return cM(n,function(t){var n=t.getData();n&&cM(n.mapDimension(e,!0),function(t){var e=n.getApproximateExtent(t);e[0]<i[0]&&(i[0]=e[0]),e[1]>i[1]&&(i[1]=e[1])})}),i[1]<i[0]&&(i=[NaN,NaN]),Gc(t,i),i}function Gc(t,e){var n=t.getAxisModel(),i=n.getMin(!0),r="category"===n.get("type"),o=r&&n.getCategories().length;null!=i&&"dataMin"!==i&&"function"!=typeof i?e[0]=i:r&&(e[0]=o>0?0:NaN);var a=n.getMax(!0);return null!=a&&"dataMax"!==a&&"function"!=typeof a?e[1]=a:r&&(e[1]=o>0?o-1:NaN),n.get("scale",!0)||(e[0]>0&&(e[0]=0),e[1]<0&&(e[1]=0)),e}function Wc(t,e){var n=t.getAxisModel(),i=t._percentWindow,r=t._valueWindow;if(i){var o=Ir(r,[0,500]);o=Math.min(o,20);var a=e||0===i[0]&&100===i[1];n.setRange(a?null:+r[0].toFixed(o),a?null:+r[1].toFixed(o))}}function Zc(t){var e=t._minMaxSpan={},n=t._dataZoomModel;cM(["min","max"],function(i){e[i+"Span"]=n.get(i+"Span");var r=n.get(i+"ValueSpan");if(null!=r&&(e[i+"ValueSpan"]=r,null!=(r=t.getAxisModel().axis.scale.parse(r)))){var o=t._dataExtent;e[i+"Span"]=xr(o[0]+r,o,[0,100],!0)}})}function Uc(t){var e={};return pM(["start","end","startValue","endValue","throttle"],function(n){t.hasOwnProperty(n)&&(e[n]=t[n])}),e}function Xc(t,e){var n=t._rangePropMode,i=t.get("rangeMode");pM([["start","startValue"],["end","endValue"]],function(t,r){var o=null!=e[t[0]],a=null!=e[t[1]];o&&!a?n[r]="percent":!o&&a?n[r]="value":i?n[r]=i[r]:o&&(n[r]="percent")})}function jc(t,e){var n=t[e]-t[1-e];return{span:Math.abs(n),sign:n>0?-1:n<0?1:e?-1:1}}function Yc(t,e){return Math.min(e[1],Math.max(e[0],t))}function qc(t){return{x:"y",y:"x",radius:"angle",angle:"radius"}[t]}function $c(t){return"vertical"===t?"ns-resize":"ew-resize"}function Kc(t,e,n){td(t)[e]=n}function Qc(t,e,n){var i=td(t);i[e]===n&&(i[e]=null)}function Jc(t,e){return!!td(t)[e]}function td(t){return t[DM]||(t[DM]={})}function ed(t){this.pointerChecker,this._zr=t,this._opt={};var e=m,i=e(nd,this),r=e(id,this),o=e(rd,this),s=e(od,this),l=e(ad,this);Zp.call(this),this.setPointerChecker=function(t){this.pointerChecker=t},this.enable=function(e,u){this.disable(),this._opt=a(n(u)||{},{zoomOnMouseWheel:!0,moveOnMouseMove:!0,preventDefaultMouseMove:!0}),null==e&&(e=!0),!0!==e&&"move"!==e&&"pan"!==e||(t.on("mousedown",i),t.on("mousemove",r),t.on("mouseup",o)),!0!==e&&"scale"!==e&&"zoom"!==e||(t.on("mousewheel",s),t.on("pinch",l))},this.disable=function(){t.off("mousedown",i),t.off("mousemove",r),t.off("mouseup",o),t.off("mousewheel",s),t.off("pinch",l)},this.dispose=this.disable,this.isDragging=function(){return this._dragging},this.isPinching=function(){return this._pinching}}function nd(t){if(!(sn(t)||t.target&&t.target.draggable)){var e=t.offsetX,n=t.offsetY;this.pointerChecker&&this.pointerChecker(t,e,n)&&(this._x=e,this._y=n,this._dragging=!0)}}function id(t){if(!sn(t)&&ld(this,"moveOnMouseMove",t)&&this._dragging&&"pinch"!==t.gestureEvent&&!Jc(this._zr,"globalPan")){var e=t.offsetX,n=t.offsetY,i=this._x,r=this._y,o=e-i,a=n-r;this._x=e,this._y=n,this._opt.preventDefaultMouseMove&&tm(t.event),this.trigger("pan",o,a,i,r,e,n)}}function rd(t){sn(t)||(this._dragging=!1)}function od(t){if(ld(this,"zoomOnMouseWheel",t)&&0!==t.wheelDelta){var e=t.wheelDelta>0?1.1:1/1.1;sd.call(this,t,e,t.offsetX,t.offsetY)}}function ad(t){if(!Jc(this._zr,"globalPan")){var e=t.pinchScale>1?1.1:1/1.1;sd.call(this,t,e,t.pinchX,t.pinchY)}}function sd(t,e,n,i){this.pointerChecker&&this.pointerChecker(t,n,i)&&(tm(t.event),this.trigger("zoom",e,n,i))}function ld(t,e,n){var i=t._opt[e];return i&&(!_(i)||n.event[i+"Key"])}function ud(t,e){var n=dd(t),i=e.dataZoomId,r=e.coordId;d(n,function(t,n){var o=t.dataZoomInfos;o[i]&&l(e.allCoordIds,r)<0&&(delete o[i],t.count--)}),pd(n);var o=n[r];o||((o=n[r]={coordId:r,dataZoomInfos:{},count:0}).controller=fd(t,o),o.dispatchAction=v(yd,t)),!o.dataZoomInfos[i]&&o.count++,o.dataZoomInfos[i]=e;var a=xd(o.dataZoomInfos);o.controller.enable(a.controlType,a.opt),o.controller.setPointerChecker(e.containsPoint),ha(o,"dispatchAction",e.throttleRate,"fixRate")}function hd(t,e){var n=dd(t);d(n,function(t){t.controller.dispose();var n=t.dataZoomInfos;n[e]&&(delete n[e],t.count--)}),pd(n)}function cd(t){return t.type+"\0_"+t.id}function dd(t){var e=t.getZr();return e[kM]||(e[kM]={})}function fd(t,e){var n=new ed(t.getZr());return n.on("pan",AM(gd,e)),n.on("zoom",AM(md,e)),n}function pd(t){d(t,function(e,n){e.count||(e.controller.dispose(),delete t[n])})}function gd(t,e,n,i,r,o,a){vd(t,function(s){return s.panGetRange(t.controller,e,n,i,r,o,a)})}function md(t,e,n,i){vd(t,function(r){return r.zoomGetRange(t.controller,e,n,i)})}function vd(t,e){var n=[];d(t.dataZoomInfos,function(t){var i=e(t);!t.disabled&&i&&n.push({dataZoomId:t.dataZoomId,start:i[0],end:i[1]})}),n.length&&t.dispatchAction(n)}function yd(t,e){t.dispatchAction({type:"dataZoom",batch:e})}function xd(t){var e,n={},i={type_true:2,type_move:1,type_false:0,type_undefined:-1};return d(t,function(t){var r=!t.disabled&&(!t.zoomLock||"move");i["type_"+r]>i["type_"+e]&&(e=r),o(n,t.roamControllerOpt)}),{controlType:e,opt:n}}function _d(t,e){zM[t]=e}function wd(t){return zM[t]}function bd(t){return 0===t.indexOf("my")}function Md(t){this.model=t}function Sd(t){this.model=t}function Id(t){var e={},n=[],i=[];return t.eachRawSeries(function(t){var r=t.coordinateSystem;if(!r||"cartesian2d"!==r.type&&"polar"!==r.type)n.push(t);else{var o=r.getBaseAxis();if("category"===o.type){var a=o.dim+"_"+o.index;e[a]||(e[a]={categoryAxis:o,valueAxis:r.getOtherAxis(o),series:[]},i.push({axisDim:o.dim,axisIndex:o.index})),e[a].series.push(t)}else n.push(t)}}),{seriesGroupByCategoryAxis:e,other:n,meta:i}}function Cd(t){var e=[];return d(t,function(t,n){var i=t.categoryAxis,r=t.valueAxis.dim,o=[" "].concat(f(t.series,function(t){return t.name})),a=[i.model.getCategories()];d(t.series,function(t){a.push(t.getRawData().mapArray(r,function(t){return t}))});for(var s=[o.join(WM)],l=0;l<a[0].length;l++){for(var u=[],h=0;h<a.length;h++)u.push(a[h][l]);s.push(u.join(WM))}e.push(s.join("\n"))}),e.join("\n\n"+GM+"\n\n")}function Td(t){return f(t,function(t){var e=t.getRawData(),n=[t.name],i=[];return e.each(e.dimensions,function(){for(var t=arguments.length,r=arguments[t-1],o=e.getName(r),a=0;a<t-1;a++)i[a]=arguments[a];n.push((o?o+WM:"")+i.join(WM))}),n.join("\n")}).join("\n\n"+GM+"\n\n")}function Dd(t){var e=Id(t);return{value:g([Cd(e.seriesGroupByCategoryAxis),Td(e.other)],function(t){return t.replace(/[\n\t\s]/g,"")}).join("\n\n"+GM+"\n\n"),meta:e.meta}}function Ad(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function kd(t){if(t.slice(0,t.indexOf("\n")).indexOf(WM)>=0)return!0}function Pd(t){for(var e=t.split(/\n+/g),n=[],i=f(Ad(e.shift()).split(ZM),function(t){return{name:t,data:[]}}),r=0;r<e.length;r++){var o=Ad(e[r]).split(ZM);n.push(o.shift());for(var a=0;a<o.length;a++)i[a]&&(i[a].data[r]=o[a])}return{series:i,categories:n}}function Ld(t){for(var e=t.split(/\n+/g),n=Ad(e.shift()),i=[],r=0;r<e.length;r++){var o,a=Ad(e[r]).split(ZM),s="",l=!1;isNaN(a[0])?(l=!0,s=a[0],a=a.slice(1),i[r]={name:s,value:[]},o=i[r].value):o=i[r]=[];for(var u=0;u<a.length;u++)o.push(+a[u]);1===o.length&&(l?i[r].value=o[0]:i[r]=o[0])}return{name:n,data:i}}function Od(t,e){var n={series:[]};return d(t.split(new RegExp("\n*"+GM+"\n*","g")),function(t,i){if(kd(t)){var r=Pd(t),o=e[i],a=o.axisDim+"Axis";o&&(n[a]=n[a]||[],n[a][o.axisIndex]={data:r.categories},n.series=n.series.concat(r.series))}else{r=Ld(t);n.series.push(r)}}),n}function zd(t){this._dom=null,this.model=t}function Ed(t,e){return f(t,function(t,n){var i=e&&e[n];return w(i)&&!y(i)?(w(t)&&!y(t)&&(t=t.value),a({value:t},i)):t})}function Nd(t){Zp.call(this),this._zr=t,this.group=new Sg,this._brushType,this._brushOption,this._panels,this._track=[],this._dragging,this._covers=[],this._creatingCover,this._creatingPanel,this._enableGlobalPan,this._uid="brushController_"+rS++,this._handlers={},XM(oS,function(t,e){this._handlers[e]=m(t,this)},this)}function Rd(t,e){var r=t._zr;t._enableGlobalPan||Kc(r,tS,t._uid),XM(t._handlers,function(t,e){r.on(e,t)}),t._brushType=e.brushType,t._brushOption=i(n(iS),e,!0)}function Bd(t){var e=t._zr;Qc(e,tS,t._uid),XM(t._handlers,function(t,n){e.off(n,t)}),t._brushType=t._brushOption=null}function Vd(t,e){var n=aS[e.brushType].createCover(t,e);return n.__brushOption=e,Gd(n,e),t.group.add(n),n}function Fd(t,e){var n=Zd(e);return n.endCreating&&(n.endCreating(t,e),Gd(e,e.__brushOption)),e}function Hd(t,e){var n=e.__brushOption;Zd(e).updateCoverShape(t,e,n.range,n)}function Gd(t,e){var n=e.z;null==n&&(n=KM),t.traverse(function(t){t.z=n,t.z2=n})}function Wd(t,e){Zd(e).updateCommon(t,e),Hd(t,e)}function Zd(t){return aS[t.__brushOption.brushType]}function Ud(t,e,n){var i=t._panels;if(!i)return!0;var r,o=t._transform;return XM(i,function(t){t.isTargetByCursor(e,n,o)&&(r=t)}),r}function Xd(t,e){var n=t._panels;if(!n)return!0;var i=e.__brushOption.panelId;return null==i||n[i]}function jd(t){var e=t._covers,n=e.length;return XM(e,function(e){t.group.remove(e)},t),e.length=0,!!n}function Yd(t,e){var i=jM(t._covers,function(t){var e=t.__brushOption,i=n(e.range);return{brushType:e.brushType,panelId:e.panelId,range:i}});t.trigger("brush",i,{isEnd:!!e.isEnd,removeOnClick:!!e.removeOnClick})}function qd(t){var e=t._track;if(!e.length)return!1;var n=e[e.length-1],i=e[0],r=n[0]-i[0],o=n[1]-i[1];return $M(r*r+o*o,.5)>QM}function $d(t){var e=t.length-1;return e<0&&(e=0),[t[0],t[e]]}function Kd(t,e,n,i){var r=new Sg;return r.add(new Fv({name:"main",style:ef(n),silent:!0,draggable:!0,cursor:"move",drift:UM(t,e,r,"nswe"),ondragend:UM(Yd,e,{isEnd:!0})})),XM(i,function(n){r.add(new Fv({name:n,style:{opacity:0},draggable:!0,silent:!0,invisible:!0,drift:UM(t,e,r,n),ondragend:UM(Yd,e,{isEnd:!0})}))}),r}function Qd(t,e,n,i){var r=i.brushStyle.lineWidth||0,o=qM(r,JM),a=n[0][0],s=n[1][0],l=a-r/2,u=s-r/2,h=n[0][1],c=n[1][1],d=h-o+r/2,f=c-o+r/2,p=h-a,g=c-s,m=p+r,v=g+r;tf(t,e,"main",a,s,p,g),i.transformable&&(tf(t,e,"w",l,u,o,v),tf(t,e,"e",d,u,o,v),tf(t,e,"n",l,u,m,o),tf(t,e,"s",l,f,m,o),tf(t,e,"nw",l,u,o,o),tf(t,e,"ne",d,u,o,o),tf(t,e,"sw",l,f,o,o),tf(t,e,"se",d,f,o,o))}function Jd(t,e){var n=e.__brushOption,i=n.transformable,r=e.childAt(0);r.useStyle(ef(n)),r.attr({silent:!i,cursor:i?"move":"default"}),XM(["w","e","n","s","se","sw","ne","nw"],function(n){var r=e.childOfName(n),o=of(t,n);r&&r.attr({silent:!i,invisible:!i,cursor:i?nS[o]+"-resize":null})})}function tf(t,e,n,i,r,o,a){var s=e.childOfName(n);s&&s.setShape(hf(uf(t,e,[[i,r],[i+o,r+a]])))}function ef(t){return a({strokeNoScale:!0},t.brushStyle)}function nf(t,e,n,i){var r=[YM(t,n),YM(e,i)],o=[qM(t,n),qM(e,i)];return[[r[0],o[0]],[r[1],o[1]]]}function rf(t){return lr(t.group)}function of(t,e){if(e.length>1)return("e"===(i=[of(t,(e=e.split(""))[0]),of(t,e[1])])[0]||"w"===i[0])&&i.reverse(),i.join("");var n={left:"w",right:"e",top:"n",bottom:"s"},i=hr({w:"left",e:"right",n:"top",s:"bottom"}[e],rf(t));return n[i]}function af(t,e,n,i,r,o,a,s){var l=i.__brushOption,u=t(l.range),h=lf(n,o,a);XM(r.split(""),function(t){var e=eS[t];u[e[0]][e[1]]+=h[e[0]]}),l.range=e(nf(u[0][0],u[1][0],u[0][1],u[1][1])),Wd(n,i),Yd(n,{isEnd:!1})}function sf(t,e,n,i,r){var o=e.__brushOption.range,a=lf(t,n,i);XM(o,function(t){t[0]+=a[0],t[1]+=a[1]}),Wd(t,e),Yd(t,{isEnd:!1})}function lf(t,e,n){var i=t.group,r=i.transformCoordToLocal(e,n),o=i.transformCoordToLocal(0,0);return[r[0]-o[0],r[1]-o[1]]}function uf(t,e,i){var r=Xd(t,e);return r&&!0!==r?r.clipPath(i,t._transform):n(i)}function hf(t){var e=YM(t[0][0],t[1][0]),n=YM(t[0][1],t[1][1]);return{x:e,y:n,width:qM(t[0][0],t[1][0])-e,height:qM(t[0][1],t[1][1])-n}}function cf(t,e,n){if(t._brushType){var i=t._zr,r=t._covers,o=Ud(t,e,n);if(!t._dragging)for(var a=0;a<r.length;a++){var s=r[a].__brushOption;if(o&&(!0===o||s.panelId===o.panelId)&&aS[s.brushType].contain(r[a],n[0],n[1]))return}o&&i.setCursorStyle("crosshair")}}function df(t){var e=t.event;e.preventDefault&&e.preventDefault()}function ff(t,e,n){return t.childOfName("main").contain(e,n)}function pf(t,e,i,r){var o,a=t._creatingCover,s=t._creatingPanel,l=t._brushOption;if(t._track.push(i.slice()),qd(t)||a){if(s&&!a){"single"===l.brushMode&&jd(t);var u=n(l);u.brushType=gf(u.brushType,s),u.panelId=!0===s?null:s.panelId,a=t._creatingCover=Vd(t,u),t._covers.push(a)}if(a){var h=aS[gf(t._brushType,s)];a.__brushOption.range=h.getCreatingRange(uf(t,a,t._track)),r&&(Fd(t,a),h.updateCommon(t,a)),Hd(t,a),o={isEnd:r}}}else r&&"single"===l.brushMode&&l.removeOnClick&&Ud(t,e,i)&&jd(t)&&(o={isEnd:r,removeOnClick:!0});return o}function gf(t,e){return"auto"===t?e.defaultBrushType:t}function mf(t){if(this._dragging){df(t);var e=pf(this,t,this.group.transformCoordToLocal(t.offsetX,t.offsetY),!0);this._dragging=!1,this._track=[],this._creatingCover=null,e&&Yd(this,e)}}function vf(t){return{createCover:function(e,n){return Kd(UM(af,function(e){var n=[e,[0,100]];return t&&n.reverse(),n},function(e){return e[t]}),e,n,[["w","e"],["n","s"]][t])},getCreatingRange:function(e){var n=$d(e);return[YM(n[0][t],n[1][t]),qM(n[0][t],n[1][t])]},updateCoverShape:function(e,n,i,r){var o,a=Xd(e,n);if(!0!==a&&a.getLinearBrushOtherExtent)o=a.getLinearBrushOtherExtent(t,e._transform);else{var s=e._zr;o=[0,[s.getWidth(),s.getHeight()][1-t]]}var l=[i,o];t&&l.reverse(),Qd(e,n,l,r)},updateCommon:Jd,contain:ff}}function yf(t,e,n){var i=e.getComponentByElement(t.topTarget),r=i&&i.coordinateSystem;return i&&i!==n&&!sS[i.mainType]&&r&&r.model!==n}function xf(t){return t=bf(t),function(e,n){return dr(e,t)}}function _f(t,e){return t=bf(t),function(n){var i=null!=e?e:n,r=i?t.width:t.height,o=i?t.x:t.y;return[o,o+(r||0)]}}function wf(t,e,n){return t=bf(t),function(i,r,o){return t.contain(r[0],r[1])&&!yf(i,e,n)}}function bf(t){return Xt.create(t)}function Mf(t,e,n){var i=this._targetInfoList=[],r={},o=If(e,t);lS(pS,function(t,e){(!n||!n.include||uS(n.include,e)>=0)&&t(o,i,r)})}function Sf(t){return t[0]>t[1]&&t.reverse(),t}function If(t,e){return An(t,e,{includeMainTypes:dS})}function Cf(t,e,n,i){var r=n.getAxis(["x","y"][t]),o=Sf(f([0,1],function(t){return e?r.coordToData(r.toLocalCoord(i[t])):r.toGlobalCoord(r.dataToCoord(i[t]))})),a=[];return a[t]=o,a[1-t]=[NaN,NaN],{values:o,xyMinMax:a}}function Tf(t,e,n,i){return[e[0]-i[t]*n[0],e[1]-i[t]*n[1]]}function Df(t,e){var n=Af(t),i=Af(e),r=[n[0]/i[0],n[1]/i[1]];return isNaN(r[0])&&(r[0]=1),isNaN(r[1])&&(r[1]=1),r}function Af(t){return t?[t[0][1]-t[0][0],t[1][1]-t[1][0]]:[NaN,NaN]}function kf(t,e){var n=zf(t);xS(e,function(e,i){for(var r=n.length-1;r>=0&&!n[r][i];r--);if(r<0){var o=t.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(o){var a=o.getPercentRange();n[0][i]={dataZoomId:i,start:a[0],end:a[1]}}}}),n.push(e)}function Pf(t){var e=zf(t),n=e[e.length-1];e.length>1&&e.pop();var i={};return xS(n,function(t,n){for(var r=e.length-1;r>=0;r--)if(t=e[r][n]){i[n]=t;break}}),i}function Lf(t){t[_S]=null}function Of(t){return zf(t).length}function zf(t){var e=t[_S];return e||(e=t[_S]=[{}]),e}function Ef(t,e,n){(this._brushController=new Nd(n.getZr())).on("brush",m(this._onBrush,this)).mount(),this._isZoomActive}function Nf(t){var e={};return d(["xAxisIndex","yAxisIndex"],function(n){e[n]=t[n],null==e[n]&&(e[n]="all"),(!1===e[n]||"none"===e[n])&&(e[n]=[])}),e}function Rf(t,e){t.setIconStatus("back",Of(e)>1?"emphasis":"normal")}function Bf(t,e,n,i,r){var o=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(o="dataZoomSelect"===i.key&&i.dataZoomSelectActive),n._isZoomActive=o,t.setIconStatus("zoom",o?"emphasis":"normal");var a=new Mf(Nf(t.option),e,{include:["grid"]});n._brushController.setPanels(a.makePanelOpts(r,function(t){return t.xAxisDeclared&&!t.yAxisDeclared?"lineX":!t.xAxisDeclared&&t.yAxisDeclared?"lineY":"rect"})).enableBrush(!!o&&{brushType:"auto",brushStyle:{lineWidth:0,fill:"rgba(0,0,0,0.2)"}})}function Vf(t){this.model=t}function Ff(t){return TS(t)}function Hf(){if(!kS&&PS){kS=!0;var t=PS.styleSheets;t.length<31?PS.createStyleSheet().addRule(".zrvml","behavior:url(#default#VML)"):t[0].addRule(".zrvml","behavior:url(#default#VML)")}}function Gf(t){return parseInt(t,10)}function Wf(t,e){Hf(),this.root=t,this.storage=e;var n=document.createElement("div"),i=document.createElement("div");n.style.cssText="display:inline-block;overflow:hidden;position:relative;width:300px;height:150px;",i.style.cssText="position:absolute;left:0;top:0;",t.appendChild(n),this._vmlRoot=i,this._vmlViewport=n,this.resize();var r=e.delFromStorage,o=e.addToStorage;e.delFromStorage=function(t){r.call(e,t),t&&t.onRemove&&t.onRemove(i)},e.addToStorage=function(t){t.onAdd&&t.onAdd(i),o.call(e,t)},this._firstPaint=!0}function Zf(t){return function(){yg('In IE8.0 VML mode painter not support method "'+t+'"')}}function Uf(t){return document.createElementNS(cI,t)}function Xf(t){return gI(1e4*t)/1e4}function jf(t){return t<wI&&t>-wI}function Yf(t,e){var n=e?t.textFill:t.fill;return null!=n&&n!==pI}function qf(t,e){var n=e?t.textStroke:t.stroke;return null!=n&&n!==pI}function $f(t,e){e&&Kf(t,"transform","matrix("+fI.call(e,",")+")")}function Kf(t,e,n){(!n||"linear"!==n.type&&"radial"!==n.type)&&("string"==typeof n&&n.indexOf("NaN")>-1&&console.log(n),t.setAttribute(e,n))}function Qf(t,e,n){t.setAttributeNS("http://www.w3.org/1999/xlink",e,n)}function Jf(t,e,n){if(Yf(e,n)){var i=n?e.textFill:e.fill;i="transparent"===i?pI:i,"none"!==t.getAttribute("clip-path")&&i===pI&&(i="rgba(0, 0, 0, 0.002)"),Kf(t,"fill",i),Kf(t,"fill-opacity",e.opacity)}else Kf(t,"fill",pI);if(qf(e,n)){var r=n?e.textStroke:e.stroke;Kf(t,"stroke",r="transparent"===r?pI:r),Kf(t,"stroke-width",(n?e.textStrokeWidth:e.lineWidth)/(!n&&e.strokeNoScale?e.host.getLineScale():1)),Kf(t,"paint-order",n?"stroke":"fill"),Kf(t,"stroke-opacity",e.opacity),e.lineDash?(Kf(t,"stroke-dasharray",e.lineDash.join(",")),Kf(t,"stroke-dashoffset",gI(e.lineDashOffset||0))):Kf(t,"stroke-dasharray",""),e.lineCap&&Kf(t,"stroke-linecap",e.lineCap),e.lineJoin&&Kf(t,"stroke-linejoin",e.lineJoin),e.miterLimit&&Kf(t,"stroke-miterlimit",e.miterLimit)}else Kf(t,"stroke",pI)}function tp(t){for(var e=[],n=t.data,i=t.len(),r=0;r<i;){var o="",a=0;switch(n[r++]){case dI.M:o="M",a=2;break;case dI.L:o="L",a=2;break;case dI.Q:o="Q",a=4;break;case dI.C:o="C",a=6;break;case dI.A:var s=n[r++],l=n[r++],u=n[r++],h=n[r++],c=n[r++],d=n[r++],f=n[r++],p=n[r++],g=Math.abs(d),m=jf(g-xI)&&!jf(g),v=!1;v=g>=xI||!jf(g)&&(d>-yI&&d<0||d>yI)==!!p;var y=Xf(s+u*vI(c)),x=Xf(l+h*mI(c));m&&(d=p?xI-1e-4:1e-4-xI,v=!0,9===r&&e.push("M",y,x));var _=Xf(s+u*vI(c+d)),w=Xf(l+h*mI(c+d));e.push("A",Xf(u),Xf(h),gI(f*_I),+v,+p,_,w);break;case dI.Z:o="Z";break;case dI.R:var _=Xf(n[r++]),w=Xf(n[r++]),b=Xf(n[r++]),M=Xf(n[r++]);e.push("M",_,w,"L",_+b,w,"L",_+b,w+M,"L",_,w+M,"L",_,w)}o&&e.push(o);for(var S=0;S<a;S++)e.push(Xf(n[r++]))}return e.join(" ")}function ep(t){return"middle"===t?"middle":"bottom"===t?"baseline":"hanging"}function np(){}function ip(t,e,n,i){for(var r=0,o=e.length,a=0,s=0;r<o;r++){var l=e[r];if(l.removed){for(var u=[],h=s;h<s+l.count;h++)u.push(h);l.indices=u,s+=l.count}else{for(var u=[],h=a;h<a+l.count;h++)u.push(h);l.indices=u,a+=l.count,l.added||(s+=l.count)}}return e}function rp(t){return{newPos:t.newPos,components:t.components.slice(0)}}function op(t,e,n,i,r){this._zrId=t,this._svgRoot=e,this._tagNames="string"==typeof n?[n]:n,this._markLabel=i,this._domName=r||"_dom",this.nextId=0}function ap(t,e){op.call(this,t,e,["linearGradient","radialGradient"],"__gradient_in_use__")}function sp(t,e){op.call(this,t,e,"clipPath","__clippath_in_use__")}function lp(t,e){op.call(this,t,e,["filter"],"__filter_in_use__","_shadowDom")}function up(t){return t&&(t.shadowBlur||t.shadowOffsetX||t.shadowOffsetY||t.textShadowBlur||t.textShadowOffsetX||t.textShadowOffsetY)}function hp(t){return parseInt(t,10)}function cp(t){return t instanceof xi?bI:t instanceof je?MI:t instanceof kv?SI:bI}function dp(t,e){return e&&t&&e.parentNode!==t}function fp(t,e,n){if(dp(t,e)&&n){var i=n.nextSibling;i?t.insertBefore(e,i):t.appendChild(e)}}function pp(t,e){if(dp(t,e)){var n=t.firstChild;n?t.insertBefore(e,n):t.appendChild(e)}}function gp(t,e){e&&t&&e.parentNode===t&&t.removeChild(e)}function mp(t){return t.__textSvgEl}function vp(t){return t.__svgEl}function yp(t){return function(){yg('In SVG mode painter not support method "'+t+'"')}}var xp=2311,_p=function(){return xp++},wp={},bp=wp="object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?{browser:{},os:{},node:!1,wxa:!0,canvasSupported:!0,svgSupported:!1,touchEventsSupported:!0}:"undefined"==typeof document&&"undefined"!=typeof self?{browser:{},os:{},node:!1,worker:!0,canvasSupported:!0}:"undefined"==typeof navigator?{browser:{},os:{},node:!0,worker:!1,canvasSupported:!0,svgSupported:!0}:function(t){var e={},n={},i=t.match(/Firefox\/([\d.]+)/),r=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),o=t.match(/Edge\/([\d.]+)/),a=/micromessenger/i.test(t);return i&&(n.firefox=!0,n.version=i[1]),r&&(n.ie=!0,n.version=r[1]),o&&(n.edge=!0,n.version=o[1]),a&&(n.weChat=!0),{browser:n,os:e,node:!1,canvasSupported:!!document.createElement("canvas").getContext,svgSupported:"undefined"!=typeof SVGRect,touchEventsSupported:"ontouchstart"in window&&!n.ie&&!n.edge,pointerEventsSupported:"onpointerdown"in window&&(n.edge||n.ie&&n.version>=11)}}(navigator.userAgent),Mp={"[object Function]":1,"[object RegExp]":1,"[object Date]":1,"[object Error]":1,"[object CanvasGradient]":1,"[object CanvasPattern]":1,"[object Image]":1,"[object Canvas]":1},Sp={"[object Int8Array]":1,"[object Uint8Array]":1,"[object Uint8ClampedArray]":1,"[object Int16Array]":1,"[object Uint16Array]":1,"[object Int32Array]":1,"[object Uint32Array]":1,"[object Float32Array]":1,"[object Float64Array]":1},Ip=Object.prototype.toString,Cp=Array.prototype,Tp=Cp.forEach,Dp=Cp.filter,Ap=Cp.slice,kp=Cp.map,Pp=Cp.reduce,Lp={},Op=function(){return Lp.createCanvas()};Lp.createCanvas=function(){return document.createElement("canvas")};var zp,Ep="__ec_primitive__";E.prototype={constructor:E,get:function(t){return this.hasOwnProperty(t)?this[t]:null},set:function(t,e){return this[t]=e},each:function(t,e){void 0!==e&&(t=m(t,e));for(var n in this)this.hasOwnProperty(n)&&t(this[n],n)},removeKey:function(t){delete this[t]}};var Np=(Object.freeze||Object)({$override:e,clone:n,merge:i,mergeAll:r,extend:o,defaults:a,createCanvas:Op,getContext:s,indexOf:l,inherits:u,mixin:h,isArrayLike:c,each:d,map:f,reduce:p,filter:g,find:function(t,e,n){if(t&&e)for(var i=0,r=t.length;i<r;i++)if(e.call(n,t[i],i,t))return t[i]},bind:m,curry:v,isArray:y,isFunction:x,isString:_,isObject:w,isBuiltInObject:b,isTypedArray:M,isDom:S,eqNaN:I,retrieve:C,retrieve2:T,retrieve3:D,slice:A,normalizeCssArray:k,assert:P,trim:L,setAsPrimitive:O,isPrimitive:z,createHashMap:N,concatArray:function(t,e){for(var n=new t.constructor(t.length+e.length),i=0;i<t.length;i++)n[i]=t[i];var r=t.length;for(i=0;i<e.length;i++)n[i+r]=e[i];return n},noop:R}),Rp="undefined"==typeof Float32Array?Array:Float32Array,Bp=Z,Vp=U,Fp=Y,Hp=q,Gp=(Object.freeze||Object)({create:B,copy:V,clone:F,set:function(t,e,n){return t[0]=e,t[1]=n,t},add:H,scaleAndAdd:G,sub:W,len:Z,length:Bp,lenSquare:U,lengthSquare:Vp,mul:function(t,e,n){return t[0]=e[0]*n[0],t[1]=e[1]*n[1],t},div:function(t,e,n){return t[0]=e[0]/n[0],t[1]=e[1]/n[1],t},dot:function(t,e){return t[0]*e[0]+t[1]*e[1]},scale:X,normalize:j,distance:Y,dist:Fp,distanceSquare:q,distSquare:Hp,negate:function(t,e){return t[0]=-e[0],t[1]=-e[1],t},lerp:function(t,e,n,i){return t[0]=e[0]+i*(n[0]-e[0]),t[1]=e[1]+i*(n[1]-e[1]),t},applyTransform:$,min:K,max:Q});J.prototype={constructor:J,_dragStart:function(t){var e=t.target;e&&e.draggable&&(this._draggingTarget=e,e.dragging=!0,this._x=t.offsetX,this._y=t.offsetY,this.dispatchToElement(tt(e,t),"dragstart",t.event))},_drag:function(t){var e=this._draggingTarget;if(e){var n=t.offsetX,i=t.offsetY,r=n-this._x,o=i-this._y;this._x=n,this._y=i,e.drift(r,o,t),this.dispatchToElement(tt(e,t),"drag",t.event);var a=this.findHover(n,i,e).target,s=this._dropTarget;this._dropTarget=a,e!==a&&(s&&a!==s&&this.dispatchToElement(tt(s,t),"dragleave",t.event),a&&a!==s&&this.dispatchToElement(tt(a,t),"dragenter",t.event))}},_dragEnd:function(t){var e=this._draggingTarget;e&&(e.dragging=!1),this.dispatchToElement(tt(e,t),"dragend",t.event),this._dropTarget&&this.dispatchToElement(tt(this._dropTarget,t),"drop",t.event),this._draggingTarget=null,this._dropTarget=null}};var Wp=Array.prototype.slice,Zp=function(){this._$handlers={}};Zp.prototype={constructor:Zp,one:function(t,e,n){var i=this._$handlers;if(!e||!t)return this;i[t]||(i[t]=[]);for(var r=0;r<i[t].length;r++)if(i[t][r].h===e)return this;return i[t].push({h:e,one:!0,ctx:n||this}),this},on:function(t,e,n){var i=this._$handlers;if(!e||!t)return this;i[t]||(i[t]=[]);for(var r=0;r<i[t].length;r++)if(i[t][r].h===e)return this;return i[t].push({h:e,one:!1,ctx:n||this}),this},isSilent:function(t){var e=this._$handlers;return e[t]&&e[t].length},off:function(t,e){var n=this._$handlers;if(!t)return this._$handlers={},this;if(e){if(n[t]){for(var i=[],r=0,o=n[t].length;r<o;r++)n[t][r].h!=e&&i.push(n[t][r]);n[t]=i}n[t]&&0===n[t].length&&delete n[t]}else delete n[t];return this},trigger:function(t){if(this._$handlers[t]){var e=arguments,n=e.length;n>3&&(e=Wp.call(e,1));for(var i=this._$handlers[t],r=i.length,o=0;o<r;){switch(n){case 1:i[o].h.call(i[o].ctx);break;case 2:i[o].h.call(i[o].ctx,e[1]);break;case 3:i[o].h.call(i[o].ctx,e[1],e[2]);break;default:i[o].h.apply(i[o].ctx,e)}i[o].one?(i.splice(o,1),r--):o++}}return this},triggerWithContext:function(t){if(this._$handlers[t]){var e=arguments,n=e.length;n>4&&(e=Wp.call(e,1,e.length-1));for(var i=e[e.length-1],r=this._$handlers[t],o=r.length,a=0;a<o;){switch(n){case 1:r[a].h.call(i);break;case 2:r[a].h.call(i,e[1]);break;case 3:r[a].h.call(i,e[1],e[2]);break;default:r[a].h.apply(i,e)}r[a].one?(r.splice(a,1),o--):a++}}return this}};var Up="silent";nt.prototype.dispose=function(){};var Xp=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],jp=function(t,e,n,i){Zp.call(this),this.storage=t,this.painter=e,this.painterRoot=i,n=n||new nt,this.proxy=null,this._hovered={},this._lastTouchMoment,this._lastX,this._lastY,J.call(this),this.setHandlerProxy(n)};jp.prototype={constructor:jp,setHandlerProxy:function(t){this.proxy&&this.proxy.dispose(),t&&(d(Xp,function(e){t.on&&t.on(e,this[e],this)},this),t.handler=this),this.proxy=t},mousemove:function(t){var e=t.zrX,n=t.zrY,i=this._hovered,r=i.target;r&&!r.__zr&&(r=(i=this.findHover(i.x,i.y)).target);var o=this._hovered=this.findHover(e,n),a=o.target,s=this.proxy;s.setCursor&&s.setCursor(a?a.cursor:"default"),r&&a!==r&&this.dispatchToElement(i,"mouseout",t),this.dispatchToElement(o,"mousemove",t),a&&a!==r&&this.dispatchToElement(o,"mouseover",t)},mouseout:function(t){this.dispatchToElement(this._hovered,"mouseout",t);var e,n=t.toElement||t.relatedTarget;do{n=n&&n.parentNode}while(n&&9!=n.nodeType&&!(e=n===this.painterRoot));!e&&this.trigger("globalout",{event:t})},resize:function(t){this._hovered={}},dispatch:function(t,e){var n=this[t];n&&n.call(this,e)},dispose:function(){this.proxy.dispose(),this.storage=this.proxy=this.painter=null},setCursorStyle:function(t){var e=this.proxy;e.setCursor&&e.setCursor(t)},dispatchToElement:function(t,e,n){var i=(t=t||{}).target;if(!i||!i.silent){for(var r="on"+e,o=et(e,t,n);i&&(i[r]&&(o.cancelBubble=i[r].call(i,o)),i.trigger(e,o),i=i.parent,!o.cancelBubble););o.cancelBubble||(this.trigger(e,o),this.painter&&this.painter.eachOtherLayer(function(t){"function"==typeof t[r]&&t[r].call(t,o),t.trigger&&t.trigger(e,o)}))}},findHover:function(t,e,n){for(var i=this.storage.getDisplayList(),r={x:t,y:e},o=i.length-1;o>=0;o--){var a;if(i[o]!==n&&!i[o].ignore&&(a=it(i[o],t,e))&&(!r.topTarget&&(r.topTarget=i[o]),a!==Up)){r.target=i[o];break}}return r}},d(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],function(t){jp.prototype[t]=function(e){var n=this.findHover(e.zrX,e.zrY),i=n.target;if("mousedown"===t)this._downEl=i,this._downPoint=[e.zrX,e.zrY],this._upEl=i;else if("mouseup"===t)this._upEl=i;else if("click"===t){if(this._downEl!==this._upEl||!this._downPoint||Fp(this._downPoint,[e.zrX,e.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,t,e)}}),h(jp,Zp),h(jp,J);var Yp="undefined"==typeof Float32Array?Array:Float32Array,qp=(Object.freeze||Object)({create:rt,identity:ot,copy:at,mul:st,translate:lt,rotate:ut,scale:ht,invert:ct,clone:function(t){var e=rt();return at(e,t),e}}),$p=ot,Kp=5e-5,Qp=function(t){(t=t||{}).position||(this.position=[0,0]),null==t.rotation&&(this.rotation=0),t.scale||(this.scale=[1,1]),this.origin=this.origin||null},Jp=Qp.prototype;Jp.transform=null,Jp.needLocalTransform=function(){return dt(this.rotation)||dt(this.position[0])||dt(this.position[1])||dt(this.scale[0]-1)||dt(this.scale[1]-1)},Jp.updateTransform=function(){var t=this.parent,e=t&&t.transform,n=this.needLocalTransform(),i=this.transform;n||e?(i=i||rt(),n?this.getLocalTransform(i):$p(i),e&&(n?st(i,t.transform,i):at(i,t.transform)),this.transform=i,this.invTransform=this.invTransform||rt(),ct(this.invTransform,i)):i&&$p(i)},Jp.getLocalTransform=function(t){return Qp.getLocalTransform(this,t)},Jp.setTransform=function(t){var e=this.transform,n=t.dpr||1;e?t.setTransform(n*e[0],n*e[1],n*e[2],n*e[3],n*e[4],n*e[5]):t.setTransform(n,0,0,n,0,0)},Jp.restoreTransform=function(t){var e=t.dpr||1;t.setTransform(e,0,0,e,0,0)};var tg=[];Jp.decomposeTransform=function(){if(this.transform){var t=this.parent,e=this.transform;t&&t.transform&&(st(tg,t.invTransform,e),e=tg);var n=e[0]*e[0]+e[1]*e[1],i=e[2]*e[2]+e[3]*e[3],r=this.position,o=this.scale;dt(n-1)&&(n=Math.sqrt(n)),dt(i-1)&&(i=Math.sqrt(i)),e[0]<0&&(n=-n),e[3]<0&&(i=-i),r[0]=e[4],r[1]=e[5],o[0]=n,o[1]=i,this.rotation=Math.atan2(-e[1]/i,e[0]/n)}},Jp.getGlobalScale=function(){var t=this.transform;if(!t)return[1,1];var e=Math.sqrt(t[0]*t[0]+t[1]*t[1]),n=Math.sqrt(t[2]*t[2]+t[3]*t[3]);return t[0]<0&&(e=-e),t[3]<0&&(n=-n),[e,n]},Jp.transformCoordToLocal=function(t,e){var n=[t,e],i=this.invTransform;return i&&$(n,n,i),n},Jp.transformCoordToGlobal=function(t,e){var n=[t,e],i=this.transform;return i&&$(n,n,i),n},Qp.getLocalTransform=function(t,e){$p(e=e||[]);var n=t.origin,i=t.scale||[1,1],r=t.rotation||0,o=t.position||[0,0];return n&&(e[4]-=n[0],e[5]-=n[1]),ht(e,e,i),r&&ut(e,e,r),n&&(e[4]+=n[0],e[5]+=n[1]),e[4]+=o[0],e[5]+=o[1],e};var eg={linear:function(t){return t},quadraticIn:function(t){return t*t},quadraticOut:function(t){return t*(2-t)},quadraticInOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)},cubicIn:function(t){return t*t*t},cubicOut:function(t){return--t*t*t+1},cubicInOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},quarticIn:function(t){return t*t*t*t},quarticOut:function(t){return 1- --t*t*t*t},quarticInOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},quinticIn:function(t){return t*t*t*t*t},quinticOut:function(t){return--t*t*t*t*t+1},quinticInOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},sinusoidalIn:function(t){return 1-Math.cos(t*Math.PI/2)},sinusoidalOut:function(t){return Math.sin(t*Math.PI/2)},sinusoidalInOut:function(t){return.5*(1-Math.cos(Math.PI*t))},exponentialIn:function(t){return 0===t?0:Math.pow(1024,t-1)},exponentialOut:function(t){return 1===t?1:1-Math.pow(2,-10*t)},exponentialInOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))},circularIn:function(t){return 1-Math.sqrt(1-t*t)},circularOut:function(t){return Math.sqrt(1- --t*t)},circularInOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},elasticIn:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4))},elasticOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/.4)+1)},elasticInOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),(t*=2)<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4)*.5+1)},backIn:function(t){var e=1.70158;return t*t*((e+1)*t-e)},backOut:function(t){var e=1.70158;return--t*t*((e+1)*t+e)+1},backInOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)},bounceIn:function(t){return 1-eg.bounceOut(1-t)},bounceOut:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},bounceInOut:function(t){return t<.5?.5*eg.bounceIn(2*t):.5*eg.bounceOut(2*t-1)+.5}};ft.prototype={constructor:ft,step:function(t,e){if(this._initialized||(this._startTime=t+this._delay,this._initialized=!0),this._paused)this._pausedTime+=e;else{var n=(t-this._startTime-this._pausedTime)/this._life;if(!(n<0)){n=Math.min(n,1);var i=this.easing,r="string"==typeof i?eg[i]:i,o="function"==typeof r?r(n):n;return this.fire("frame",o),1==n?this.loop?(this.restart(t),"restart"):(this._needsRemove=!0,"destroy"):null}}},restart:function(t){var e=(t-this._startTime-this._pausedTime)%this._life;this._startTime=t-e+this.gap,this._pausedTime=0,this._needsRemove=!1},fire:function(t,e){this[t="on"+t]&&this[t](this._target,e)},pause:function(){this._paused=!0},resume:function(){this._paused=!1}};var ng=function(){this.head=null,this.tail=null,this._len=0},ig=ng.prototype;ig.insert=function(t){var e=new rg(t);return this.insertEntry(e),e},ig.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},ig.remove=function(t){var e=t.prev,n=t.next;e?e.next=n:this.head=n,n?n.prev=e:this.tail=e,t.next=t.prev=null,this._len--},ig.len=function(){return this._len},ig.clear=function(){this.head=this.tail=null,this._len=0};var rg=function(t){this.value=t,this.next,this.prev},og=function(t){this._list=new ng,this._map={},this._maxSize=t||10,this._lastRemovedEntry=null},ag=og.prototype;ag.put=function(t,e){var n=this._list,i=this._map,r=null;if(null==i[t]){var o=n.len(),a=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var s=n.head;n.remove(s),delete i[s.key],r=s.value,this._lastRemovedEntry=s}a?a.value=e:a=new rg(e),a.key=t,n.insertEntry(a),i[t]=a}return r},ag.get=function(t){var e=this._map[t],n=this._list;if(null!=e)return e!==n.tail&&(n.remove(e),n.insertEntry(e)),e.value},ag.clear=function(){this._list.clear(),this._map={}};var sg={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]},lg=new og(20),ug=null,hg=At,cg=kt,dg=(Object.freeze||Object)({parse:St,lift:Tt,toHex:Dt,fastLerp:At,fastMapToColor:hg,lerp:kt,mapToColor:cg,modifyHSL:function(t,e,n,i){if(t=St(t))return t=Ct(t),null!=e&&(t[0]=gt(e)),null!=n&&(t[1]=yt(n)),null!=i&&(t[2]=yt(i)),Lt(It(t),"rgba")},modifyAlpha:Pt,stringify:Lt}),fg=Array.prototype.slice,pg=function(t,e,n,i){this._tracks={},this._target=t,this._loop=e||!1,this._getter=n||Ot,this._setter=i||zt,this._clipCount=0,this._delay=0,this._doneList=[],this._onframeList=[],this._clipList=[]};pg.prototype={when:function(t,e){var n=this._tracks;for(var i in e)if(e.hasOwnProperty(i)){if(!n[i]){n[i]=[];var r=this._getter(this._target,i);if(null==r)continue;0!==t&&n[i].push({time:0,value:Gt(r)})}n[i].push({time:t,value:e[i]})}return this},during:function(t){return this._onframeList.push(t),this},pause:function(){for(var t=0;t<this._clipList.length;t++)this._clipList[t].pause();this._paused=!0},resume:function(){for(var t=0;t<this._clipList.length;t++)this._clipList[t].resume();this._paused=!1},isPaused:function(){return!!this._paused},_doneCallback:function(){this._tracks={},this._clipList.length=0;for(var t=this._doneList,e=t.length,n=0;n<e;n++)t[n].call(this)},start:function(t,e){var n,i=this,r=0;for(var o in this._tracks)if(this._tracks.hasOwnProperty(o)){var a=Ut(this,t,function(){--r||i._doneCallback()},this._tracks[o],o,e);a&&(this._clipList.push(a),r++,this.animation&&this.animation.addClip(a),n=a)}if(n){var s=n.onframe;n.onframe=function(t,e){s(t,e);for(var n=0;n<i._onframeList.length;n++)i._onframeList[n](t,e)}}return r||this._doneCallback(),this},stop:function(t){for(var e=this._clipList,n=this.animation,i=0;i<e.length;i++){var r=e[i];t&&r.onframe(this._target,1),n&&n.removeClip(r)}e.length=0},delay:function(t){return this._delay=t,this},done:function(t){return t&&this._doneList.push(t),this},getClips:function(){return this._clipList}};var gg=1;"undefined"!=typeof window&&(gg=Math.max(window.devicePixelRatio||1,1));var mg=gg,vg=function(){},yg=vg,xg=function(){this.animators=[]};xg.prototype={constructor:xg,animate:function(t,e){var n,i=!1,r=this,o=this.__zr;if(t){var a=t.split("."),s=r;i="shape"===a[0];for(var u=0,h=a.length;u<h;u++)s&&(s=s[a[u]]);s&&(n=s)}else n=r;if(n){var c=r.animators,d=new pg(n,e);return d.during(function(t){r.dirty(i)}).done(function(){c.splice(l(c,d),1)}),c.push(d),o&&o.animation.addAnimator(d),d}yg('Property "'+t+'" is not existed in element '+r.id)},stopAnimation:function(t){for(var e=this.animators,n=e.length,i=0;i<n;i++)e[i].stop(t);return e.length=0,this},animateTo:function(t,e,n,i,r,o){_(n)?(r=i,i=n,n=0):x(i)?(r=i,i="linear",n=0):x(n)?(r=n,n=0):x(e)?(r=e,e=500):e||(e=500),this.stopAnimation(),this._animateToShallow("",this,t,e,n);var a=this.animators.slice(),s=a.length;s||r&&r();for(var l=0;l<a.length;l++)a[l].done(function(){--s||r&&r()}).start(i,o)},_animateToShallow:function(t,e,n,i,r){var o={},a=0;for(var s in n)if(n.hasOwnProperty(s))if(null!=e[s])w(n[s])&&!c(n[s])?this._animateToShallow(t?t+"."+s:s,e[s],n[s],i,r):(o[s]=n[s],a++);else if(null!=n[s])if(t){var l={};l[t]={},l[t][s]=n[s],this.attr(l)}else this.attr(s,n[s]);return a>0&&this.animate(t,!1).when(null==i?500:i,o).delay(r||0),this}};var _g=function(t){Qp.call(this,t),Zp.call(this,t),xg.call(this,t),this.id=t.id||_p()};_g.prototype={type:"element",name:"",__zr:null,ignore:!1,clipPath:null,isGroup:!1,drift:function(t,e){switch(this.draggable){case"horizontal":e=0;break;case"vertical":t=0}var n=this.transform;n||(n=this.transform=[1,0,0,1,0,0]),n[4]+=t,n[5]+=e,this.decomposeTransform(),this.dirty(!1)},beforeUpdate:function(){},afterUpdate:function(){},update:function(){this.updateTransform()},traverse:function(t,e){},attrKV:function(t,e){if("position"===t||"scale"===t||"origin"===t){if(e){var n=this[t];n||(n=this[t]=[]),n[0]=e[0],n[1]=e[1]}}else this[t]=e},hide:function(){this.ignore=!0,this.__zr&&this.__zr.refresh()},show:function(){this.ignore=!1,this.__zr&&this.__zr.refresh()},attr:function(t,e){if("string"==typeof t)this.attrKV(t,e);else if(w(t))for(var n in t)t.hasOwnProperty(n)&&this.attrKV(n,t[n]);return this.dirty(!1),this},setClipPath:function(t){var e=this.__zr;e&&t.addSelfToZr(e),this.clipPath&&this.clipPath!==t&&this.removeClipPath(),this.clipPath=t,t.__zr=e,t.__clipTarget=this,this.dirty(!1)},removeClipPath:function(){var t=this.clipPath;t&&(t.__zr&&t.removeSelfFromZr(t.__zr),t.__zr=null,t.__clipTarget=null,this.clipPath=null,this.dirty(!1))},addSelfToZr:function(t){this.__zr=t;var e=this.animators;if(e)for(var n=0;n<e.length;n++)t.animation.addAnimator(e[n]);this.clipPath&&this.clipPath.addSelfToZr(t)},removeSelfFromZr:function(t){this.__zr=null;var e=this.animators;if(e)for(var n=0;n<e.length;n++)t.animation.removeAnimator(e[n]);this.clipPath&&this.clipPath.removeSelfFromZr(t)}},h(_g,xg),h(_g,Qp),h(_g,Zp);var wg=$,bg=Math.min,Mg=Math.max;Xt.prototype={constructor:Xt,union:function(t){var e=bg(t.x,this.x),n=bg(t.y,this.y);this.width=Mg(t.x+t.width,this.x+this.width)-e,this.height=Mg(t.y+t.height,this.y+this.height)-n,this.x=e,this.y=n},applyTransform:function(){var t=[],e=[],n=[],i=[];return function(r){if(r){t[0]=n[0]=this.x,t[1]=i[1]=this.y,e[0]=i[0]=this.x+this.width,e[1]=n[1]=this.y+this.height,wg(t,t,r),wg(e,e,r),wg(n,n,r),wg(i,i,r),this.x=bg(t[0],e[0],n[0],i[0]),this.y=bg(t[1],e[1],n[1],i[1]);var o=Mg(t[0],e[0],n[0],i[0]),a=Mg(t[1],e[1],n[1],i[1]);this.width=o-this.x,this.height=a-this.y}}}(),calculateTransform:function(t){var e=this,n=t.width/e.width,i=t.height/e.height,r=rt();return lt(r,r,[-e.x,-e.y]),ht(r,r,[n,i]),lt(r,r,[t.x,t.y]),r},intersect:function(t){if(!t)return!1;t instanceof Xt||(t=Xt.create(t));var e=this,n=e.x,i=e.x+e.width,r=e.y,o=e.y+e.height,a=t.x,s=t.x+t.width,l=t.y,u=t.y+t.height;return!(i<a||s<n||o<l||u<r)},contain:function(t,e){var n=this;return t>=n.x&&t<=n.x+n.width&&e>=n.y&&e<=n.y+n.height},clone:function(){return new Xt(this.x,this.y,this.width,this.height)},copy:function(t){this.x=t.x,this.y=t.y,this.width=t.width,this.height=t.height},plain:function(){return{x:this.x,y:this.y,width:this.width,height:this.height}}},Xt.create=function(t){return new Xt(t.x,t.y,t.width,t.height)};var Sg=function(t){t=t||{},_g.call(this,t);for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e]);this._children=[],this.__storage=null,this.__dirty=!0};Sg.prototype={constructor:Sg,isGroup:!0,type:"group",silent:!1,children:function(){return this._children.slice()},childAt:function(t){return this._children[t]},childOfName:function(t){for(var e=this._children,n=0;n<e.length;n++)if(e[n].name===t)return e[n]},childCount:function(){return this._children.length},add:function(t){return t&&t!==this&&t.parent!==this&&(this._children.push(t),this._doAdd(t)),this},addBefore:function(t,e){if(t&&t!==this&&t.parent!==this&&e&&e.parent===this){var n=this._children,i=n.indexOf(e);i>=0&&(n.splice(i,0,t),this._doAdd(t))}return this},_doAdd:function(t){t.parent&&t.parent.remove(t),t.parent=this;var e=this.__storage,n=this.__zr;e&&e!==t.__storage&&(e.addToStorage(t),t instanceof Sg&&t.addChildrenToStorage(e)),n&&n.refresh()},remove:function(t){var e=this.__zr,n=this.__storage,i=this._children,r=l(i,t);return r<0?this:(i.splice(r,1),t.parent=null,n&&(n.delFromStorage(t),t instanceof Sg&&t.delChildrenFromStorage(n)),e&&e.refresh(),this)},removeAll:function(){var t,e,n=this._children,i=this.__storage;for(e=0;e<n.length;e++)t=n[e],i&&(i.delFromStorage(t),t instanceof Sg&&t.delChildrenFromStorage(i)),t.parent=null;return n.length=0,this},eachChild:function(t,e){for(var n=this._children,i=0;i<n.length;i++){var r=n[i];t.call(e,r,i)}return this},traverse:function(t,e){for(var n=0;n<this._children.length;n++){var i=this._children[n];t.call(e,i),"group"===i.type&&i.traverse(t,e)}return this},addChildrenToStorage:function(t){for(var e=0;e<this._children.length;e++){var n=this._children[e];t.addToStorage(n),n instanceof Sg&&n.addChildrenToStorage(t)}},delChildrenFromStorage:function(t){for(var e=0;e<this._children.length;e++){var n=this._children[e];t.delFromStorage(n),n instanceof Sg&&n.delChildrenFromStorage(t)}},dirty:function(){return this.__dirty=!0,this.__zr&&this.__zr.refresh(),this},getBoundingRect:function(t){for(var e=null,n=new Xt(0,0,0,0),i=t||this._children,r=[],o=0;o<i.length;o++){var a=i[o];if(!a.ignore&&!a.invisible){var s=a.getBoundingRect(),l=a.getLocalTransform(r);l?(n.copy(s),n.applyTransform(l),(e=e||n.clone()).union(n)):(e=e||s.clone()).union(s)}}return e||n}},u(Sg,_g);var Ig=32,Cg=7,Tg=function(){this._roots=[],this._displayList=[],this._displayListLen=0};Tg.prototype={constructor:Tg,traverse:function(t,e){for(var n=0;n<this._roots.length;n++)this._roots[n].traverse(t,e)},getDisplayList:function(t,e){return e=e||!1,t&&this.updateDisplayList(e),this._displayList},updateDisplayList:function(t){this._displayListLen=0;for(var e=this._roots,n=this._displayList,i=0,r=e.length;i<r;i++)this._updateAndAddDisplayable(e[i],null,t);n.length=this._displayListLen,bp.canvasSupported&&te(n,ee)},_updateAndAddDisplayable:function(t,e,n){if(!t.ignore||n){t.beforeUpdate(),t.__dirty&&t.update(),t.afterUpdate();var i=t.clipPath;if(i){e=e?e.slice():[];for(var r=i,o=t;r;)r.parent=o,r.updateTransform(),e.push(r),o=r,r=r.clipPath}if(t.isGroup){for(var a=t._children,s=0;s<a.length;s++){var l=a[s];t.__dirty&&(l.__dirty=!0),this._updateAndAddDisplayable(l,e,n)}t.__dirty=!1}else t.__clipPaths=e,this._displayList[this._displayListLen++]=t}},addRoot:function(t){t.__storage!==this&&(t instanceof Sg&&t.addChildrenToStorage(this),this.addToStorage(t),this._roots.push(t))},delRoot:function(t){if(null==t){for(n=0;n<this._roots.length;n++){var e=this._roots[n];e instanceof Sg&&e.delChildrenFromStorage(this)}return this._roots=[],this._displayList=[],void(this._displayListLen=0)}if(t instanceof Array)for(var n=0,i=t.length;n<i;n++)this.delRoot(t[n]);else{var r=l(this._roots,t);r>=0&&(this.delFromStorage(t),this._roots.splice(r,1),t instanceof Sg&&t.delChildrenFromStorage(this))}},addToStorage:function(t){return t&&(t.__storage=this,t.dirty(!1)),this},delFromStorage:function(t){return t&&(t.__storage=null),this},dispose:function(){this._renderList=this._roots=null},displayableSortFunc:ee};var Dg={shadowBlur:1,shadowOffsetX:1,shadowOffsetY:1,textShadowBlur:1,textShadowOffsetX:1,textShadowOffsetY:1,textBoxShadowBlur:1,textBoxShadowOffsetX:1,textBoxShadowOffsetY:1},Ag=function(t,e,n){return Dg.hasOwnProperty(e)?n*=t.dpr:n},kg=[["shadowBlur",0],["shadowOffsetX",0],["shadowOffsetY",0],["shadowColor","#000"],["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]],Pg=function(t,e){this.extendFrom(t,!1),this.host=e};Pg.prototype={constructor:Pg,host:null,fill:"#000",stroke:null,opacity:1,lineDash:null,lineDashOffset:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,lineWidth:1,strokeNoScale:!1,text:null,font:null,textFont:null,fontStyle:null,fontWeight:null,fontSize:null,fontFamily:null,textTag:null,textFill:"#000",textStroke:null,textWidth:null,textHeight:null,textStrokeWidth:0,textLineHeight:null,textPosition:"inside",textRect:null,textOffset:null,textAlign:null,textVerticalAlign:null,textDistance:5,textShadowColor:"transparent",textShadowBlur:0,textShadowOffsetX:0,textShadowOffsetY:0,textBoxShadowColor:"transparent",textBoxShadowBlur:0,textBoxShadowOffsetX:0,textBoxShadowOffsetY:0,transformText:!1,textRotation:0,textOrigin:null,textBackgroundColor:null,textBorderColor:null,textBorderWidth:0,textBorderRadius:0,textPadding:null,rich:null,truncate:null,blend:null,bind:function(t,e,n){for(var i=this,r=n&&n.style,o=!r,a=0;a<kg.length;a++){var s=kg[a],l=s[0];(o||i[l]!==r[l])&&(t[l]=Ag(t,l,i[l]||s[1]))}if((o||i.fill!==r.fill)&&(t.fillStyle=i.fill),(o||i.stroke!==r.stroke)&&(t.strokeStyle=i.stroke),(o||i.opacity!==r.opacity)&&(t.globalAlpha=null==i.opacity?1:i.opacity),(o||i.blend!==r.blend)&&(t.globalCompositeOperation=i.blend||"source-over"),this.hasStroke()){var u=i.lineWidth;t.lineWidth=u/(this.strokeNoScale&&e&&e.getLineScale?e.getLineScale():1)}},hasFill:function(){var t=this.fill;return null!=t&&"none"!==t},hasStroke:function(){var t=this.stroke;return null!=t&&"none"!==t&&this.lineWidth>0},extendFrom:function(t,e){if(t)for(var n in t)!t.hasOwnProperty(n)||!0!==e&&(!1===e?this.hasOwnProperty(n):null==t[n])||(this[n]=t[n])},set:function(t,e){"string"==typeof t?this[t]=e:this.extendFrom(t,!0)},clone:function(){var t=new this.constructor;return t.extendFrom(this,!0),t},getGradient:function(t,e,n){for(var i=("radial"===e.type?ie:ne)(t,e,n),r=e.colorStops,o=0;o<r.length;o++)i.addColorStop(r[o].offset,r[o].color);return i}};for(var Lg=Pg.prototype,Og=0;Og<kg.length;Og++){var zg=kg[Og];zg[0]in Lg||(Lg[zg[0]]=zg[1])}Pg.getGradient=Lg.getGradient;var Eg=function(t,e){this.image=t,this.repeat=e,this.type="pattern"};Eg.prototype.getCanvasPattern=function(t){return t.createPattern(this.image,this.repeat||"repeat")};var Ng=function(t,e,n){var i;n=n||mg,"string"==typeof t?i=oe(t,e,n):w(t)&&(t=(i=t).id),this.id=t,this.dom=i;var r=i.style;r&&(i.onselectstart=re,r["-webkit-user-select"]="none",r["user-select"]="none",r["-webkit-touch-callout"]="none",r["-webkit-tap-highlight-color"]="rgba(0,0,0,0)",r.padding=0,r.margin=0,r["border-width"]=0),this.domBack=null,this.ctxBack=null,this.painter=e,this.config=null,this.clearColor=0,this.motionBlur=!1,this.lastFrameAlpha=.7,this.dpr=n};Ng.prototype={constructor:Ng,__dirty:!0,__used:!1,__drawIndex:0,__startIndex:0,__endIndex:0,incremental:!1,getElementCount:function(){return this.__endIndex-this.__startIndex},initContext:function(){this.ctx=this.dom.getContext("2d"),this.ctx.dpr=this.dpr},createBackBuffer:function(){var t=this.dpr;this.domBack=oe("back-"+this.id,this.painter,t),this.ctxBack=this.domBack.getContext("2d"),1!=t&&this.ctxBack.scale(t,t)},resize:function(t,e){var n=this.dpr,i=this.dom,r=i.style,o=this.domBack;r&&(r.width=t+"px",r.height=e+"px"),i.width=t*n,i.height=e*n,o&&(o.width=t*n,o.height=e*n,1!=n&&this.ctxBack.scale(n,n))},clear:function(t,e){var n=this.dom,i=this.ctx,r=n.width,o=n.height,e=e||this.clearColor,a=this.motionBlur&&!t,s=this.lastFrameAlpha,l=this.dpr;if(a&&(this.domBack||this.createBackBuffer(),this.ctxBack.globalCompositeOperation="copy",this.ctxBack.drawImage(n,0,0,r/l,o/l)),i.clearRect(0,0,r,o),e&&"transparent"!==e){var u;e.colorStops?(u=e.__canvasGradient||Pg.getGradient(i,e,{x:0,y:0,width:r,height:o}),e.__canvasGradient=u):e.image&&(u=Eg.prototype.getCanvasPattern.call(e,i)),i.save(),i.fillStyle=u||e,i.fillRect(0,0,r,o),i.restore()}if(a){var h=this.domBack;i.save(),i.globalAlpha=s,i.drawImage(h,0,0,r,o),i.restore()}}};var Rg="undefined"!=typeof window&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){setTimeout(t,16)},Bg=new og(50),Vg={},Fg=0,Hg=5e3,Gg=/\{([a-zA-Z0-9_]+)\|([^}]*)\}/g,Wg="12px sans-serif",Zg={};Zg.measureText=function(t,e){var n=s();return n.font=e||Wg,n.measureText(t)};var Ug={left:1,right:1,center:1},Xg={top:1,bottom:1,middle:1},jg=new Xt,Yg=function(){};Yg.prototype={constructor:Yg,drawRectText:function(t,e){var n=this.style;e=n.textRect||e,this.__dirty&&De(n);var i=n.text;if(null!=i&&(i+=""),Ue(i,n)){t.save();var r=this.transform;n.transformText?this.setTransform(t):r&&(jg.copy(e),jg.applyTransform(r),e=jg),ke(this,t,i,n,e),t.restore()}}},Xe.prototype={constructor:Xe,type:"displayable",__dirty:!0,invisible:!1,z:0,z2:0,zlevel:0,draggable:!1,dragging:!1,silent:!1,culling:!1,cursor:"pointer",rectHover:!1,progressive:!1,incremental:!1,inplace:!1,beforeBrush:function(t){},afterBrush:function(t){},brush:function(t,e){},getBoundingRect:function(){},contain:function(t,e){return this.rectContain(t,e)},traverse:function(t,e){t.call(e,this)},rectContain:function(t,e){var n=this.transformCoordToLocal(t,e);return this.getBoundingRect().contain(n[0],n[1])},dirty:function(){this.__dirty=!0,this._rect=null,this.__zr&&this.__zr.refresh()},animateStyle:function(t){return this.animate("style",t)},attrKV:function(t,e){"style"!==t?_g.prototype.attrKV.call(this,t,e):this.style.set(e)},setStyle:function(t,e){return this.style.set(t,e),this.dirty(!1),this},useStyle:function(t){return this.style=new Pg(t,this),this.dirty(!1),this}},u(Xe,_g),h(Xe,Yg),je.prototype={constructor:je,type:"image",brush:function(t,e){var n=this.style,i=n.image;n.bind(t,this,e);var r=this._image=se(i,this._image,this,this.onload);if(r&&ue(r)){var o=n.x||0,a=n.y||0,s=n.width,l=n.height,u=r.width/r.height;if(null==s&&null!=l?s=l*u:null==l&&null!=s?l=s/u:null==s&&null==l&&(s=r.width,l=r.height),this.setTransform(t),n.sWidth&&n.sHeight){var h=n.sx||0,c=n.sy||0;t.drawImage(r,h,c,n.sWidth,n.sHeight,o,a,s,l)}else if(n.sx&&n.sy){var d=s-(h=n.sx),f=l-(c=n.sy);t.drawImage(r,h,c,d,f,o,a,s,l)}else t.drawImage(r,o,a,s,l);null!=n.text&&(this.restoreTransform(t),this.drawRectText(t,this.getBoundingRect()))}},getBoundingRect:function(){var t=this.style;return this._rect||(this._rect=new Xt(t.x||0,t.y||0,t.width||0,t.height||0)),this._rect}},u(je,Xe);var qg=new Xt(0,0,0,0),$g=new Xt(0,0,0,0),Kg=function(t,e,n){this.type="canvas";var i=!t.nodeName||"CANVAS"===t.nodeName.toUpperCase();this._opts=n=o({},n||{}),this.dpr=n.devicePixelRatio||mg,this._singleCanvas=i,this.root=t;var r=t.style;r&&(r["-webkit-tap-highlight-color"]="transparent",r["-webkit-user-select"]=r["user-select"]=r["-webkit-touch-callout"]="none",t.innerHTML=""),this.storage=e;var a=this._zlevelList=[],s=this._layers={};if(this._layerConfig={},this._needsManuallyCompositing=!1,i){var l=t.width,u=t.height;null!=n.width&&(l=n.width),null!=n.height&&(u=n.height),this.dpr=n.devicePixelRatio||1,t.width=l*this.dpr,t.height=u*this.dpr,this._width=l,this._height=u;var h=new Ng(t,this,this.dpr);h.__builtin__=!0,h.initContext(),s[314159]=h,h.zlevel=314159,a.push(314159),this._domRoot=t}else{this._width=this._getSize(0),this._height=this._getSize(1);var c=this._domRoot=Je(this._width,this._height);t.appendChild(c)}this._hoverlayer=null,this._hoverElements=[]};Kg.prototype={constructor:Kg,getType:function(){return"canvas"},isSingleCanvas:function(){return this._singleCanvas},getViewportRoot:function(){return this._domRoot},getViewportRootOffset:function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},refresh:function(t){var e=this.storage.getDisplayList(!0),n=this._zlevelList;this._redrawId=Math.random(),this._paintList(e,t,this._redrawId);for(var i=0;i<n.length;i++){var r=n[i],o=this._layers[r];if(!o.__builtin__&&o.refresh){var a=0===i?this._backgroundColor:null;o.refresh(a)}}return this.refreshHover(),this},addHover:function(t,e){if(!t.__hoverMir){var n=new t.constructor({style:t.style,shape:t.shape});n.__from=t,t.__hoverMir=n,n.setStyle(e),this._hoverElements.push(n)}},removeHover:function(t){var e=t.__hoverMir,n=this._hoverElements,i=l(n,e);i>=0&&n.splice(i,1),t.__hoverMir=null},clearHover:function(t){for(var e=this._hoverElements,n=0;n<e.length;n++){var i=e[n].__from;i&&(i.__hoverMir=null)}e.length=0},refreshHover:function(){var t=this._hoverElements,e=t.length,n=this._hoverlayer;if(n&&n.clear(),e){te(t,this.storage.displayableSortFunc),n||(n=this._hoverlayer=this.getLayer(1e5));var i={};n.ctx.save();for(var r=0;r<e;){var o=t[r],a=o.__from;a&&a.__zr?(r++,a.invisible||(o.transform=a.transform,o.invTransform=a.invTransform,o.__clipPaths=a.__clipPaths,this._doPaintEl(o,n,!0,i))):(t.splice(r,1),a.__hoverMir=null,e--)}n.ctx.restore()}},getHoverLayer:function(){return this.getLayer(1e5)},_paintList:function(t,e,n){if(this._redrawId===n){e=e||!1,this._updateLayerStatus(t);var i=this._doPaintList(t,e);if(this._needsManuallyCompositing&&this._compositeManually(),!i){var r=this;Rg(function(){r._paintList(t,e,n)})}}},_compositeManually:function(){var t=this.getLayer(314159).ctx,e=this._domRoot.width,n=this._domRoot.height;t.clearRect(0,0,e,n),this.eachBuiltinLayer(function(i){i.virtual&&t.drawImage(i.dom,0,0,e,n)})},_doPaintList:function(t,e){for(var n=[],i=0;i<this._zlevelList.length;i++){var r=this._zlevelList[i];(s=this._layers[r]).__builtin__&&s!==this._hoverlayer&&(s.__dirty||e)&&n.push(s)}for(var o=!0,a=0;a<n.length;a++){var s=n[a],l=s.ctx,u={};l.save();var h=e?s.__startIndex:s.__drawIndex,c=!e&&s.incremental&&Date.now,f=c&&Date.now(),p=s.zlevel===this._zlevelList[0]?this._backgroundColor:null;if(s.__startIndex===s.__endIndex)s.clear(!1,p);else if(h===s.__startIndex){var g=t[h];g.incremental&&g.notClear&&!e||s.clear(!1,p)}-1===h&&(console.error("For some unknown reason. drawIndex is -1"),h=s.__startIndex);for(var m=h;m<s.__endIndex;m++){var v=t[m];if(this._doPaintEl(v,s,e,u),v.__dirty=!1,c&&Date.now()-f>15)break}s.__drawIndex=m,s.__drawIndex<s.__endIndex&&(o=!1),u.prevElClipPaths&&l.restore(),l.restore()}return bp.wxa&&d(this._layers,function(t){t&&t.ctx&&t.ctx.draw&&t.ctx.draw()}),o},_doPaintEl:function(t,e,n,i){var r=e.ctx,o=t.transform;if((e.__dirty||n)&&!t.invisible&&0!==t.style.opacity&&(!o||o[0]||o[3])&&(!t.culling||!$e(t,this._width,this._height))){var a=t.__clipPaths;i.prevElClipPaths&&!Ke(a,i.prevElClipPaths)||(i.prevElClipPaths&&(e.ctx.restore(),i.prevElClipPaths=null,i.prevEl=null),a&&(r.save(),Qe(a,r),i.prevElClipPaths=a)),t.beforeBrush&&t.beforeBrush(r),t.brush(r,i.prevEl||null),i.prevEl=t,t.afterBrush&&t.afterBrush(r)}},getLayer:function(t,e){this._singleCanvas&&!this._needsManuallyCompositing&&(t=314159);var n=this._layers[t];return n||((n=new Ng("zr_"+t,this,this.dpr)).zlevel=t,n.__builtin__=!0,this._layerConfig[t]&&i(n,this._layerConfig[t],!0),e&&(n.virtual=e),this.insertLayer(t,n),n.initContext()),n},insertLayer:function(t,e){var n=this._layers,i=this._zlevelList,r=i.length,o=null,a=-1,s=this._domRoot;if(n[t])yg("ZLevel "+t+" has been used already");else if(qe(e)){if(r>0&&t>i[0]){for(a=0;a<r-1&&!(i[a]<t&&i[a+1]>t);a++);o=n[i[a]]}if(i.splice(a+1,0,t),n[t]=e,!e.virtual)if(o){var l=o.dom;l.nextSibling?s.insertBefore(e.dom,l.nextSibling):s.appendChild(e.dom)}else s.firstChild?s.insertBefore(e.dom,s.firstChild):s.appendChild(e.dom)}else yg("Layer of zlevel "+t+" is not valid")},eachLayer:function(t,e){var n,i,r=this._zlevelList;for(i=0;i<r.length;i++)n=r[i],t.call(e,this._layers[n],n)},eachBuiltinLayer:function(t,e){var n,i,r,o=this._zlevelList;for(r=0;r<o.length;r++)i=o[r],(n=this._layers[i]).__builtin__&&t.call(e,n,i)},eachOtherLayer:function(t,e){var n,i,r,o=this._zlevelList;for(r=0;r<o.length;r++)i=o[r],(n=this._layers[i]).__builtin__||t.call(e,n,i)},getLayers:function(){return this._layers},_updateLayerStatus:function(t){function e(t){n&&(n.__endIndex!==t&&(n.__dirty=!0),n.__endIndex=t)}if(this.eachBuiltinLayer(function(t,e){t.__dirty=t.__used=!1}),this._singleCanvas)for(r=1;r<t.length;r++)if((a=t[r]).zlevel!==t[r-1].zlevel||a.incremental){this._needsManuallyCompositing=!0;break}for(var n=null,i=0,r=0;r<t.length;r++){var o,a=t[r],s=a.zlevel;a.incremental?((o=this.getLayer(s+.001,this._needsManuallyCompositing)).incremental=!0,i=1):o=this.getLayer(s+(i>0?.01:0),this._needsManuallyCompositing),o.__builtin__||yg("ZLevel "+s+" has been used by unkown layer "+o.id),o!==n&&(o.__used=!0,o.__startIndex!==r&&(o.__dirty=!0),o.__startIndex=r,o.incremental?o.__drawIndex=-1:o.__drawIndex=r,e(r),n=o),a.__dirty&&(o.__dirty=!0,o.incremental&&o.__drawIndex<0&&(o.__drawIndex=r))}e(r),this.eachBuiltinLayer(function(t,e){!t.__used&&t.getElementCount()>0&&(t.__dirty=!0,t.__startIndex=t.__endIndex=t.__drawIndex=0),t.__dirty&&t.__drawIndex<0&&(t.__drawIndex=t.__startIndex)})},clear:function(){return this.eachBuiltinLayer(this._clearLayer),this},_clearLayer:function(t){t.clear()},setBackgroundColor:function(t){this._backgroundColor=t},configLayer:function(t,e){if(e){var n=this._layerConfig;n[t]?i(n[t],e,!0):n[t]=e;for(var r=0;r<this._zlevelList.length;r++){var o=this._zlevelList[r];o!==t&&o!==t+.01||i(this._layers[o],n[t],!0)}}},delLayer:function(t){var e=this._layers,n=this._zlevelList,i=e[t];i&&(i.dom.parentNode.removeChild(i.dom),delete e[t],n.splice(l(n,t),1))},resize:function(t,e){if(this._domRoot.style){var n=this._domRoot;n.style.display="none";var i=this._opts;if(null!=t&&(i.width=t),null!=e&&(i.height=e),t=this._getSize(0),e=this._getSize(1),n.style.display="",this._width!=t||e!=this._height){n.style.width=t+"px",n.style.height=e+"px";for(var r in this._layers)this._layers.hasOwnProperty(r)&&this._layers[r].resize(t,e);d(this._progressiveLayers,function(n){n.resize(t,e)}),this.refresh(!0)}this._width=t,this._height=e}else{if(null==t||null==e)return;this._width=t,this._height=e,this.getLayer(314159).resize(t,e)}return this},clearLayer:function(t){var e=this._layers[t];e&&e.clear()},dispose:function(){this.root.innerHTML="",this.root=this.storage=this._domRoot=this._layers=null},getRenderedCanvas:function(t){if(t=t||{},this._singleCanvas&&!this._compositeManually)return this._layers[314159].dom;var e=new Ng("image",this,t.pixelRatio||this.dpr);if(e.initContext(),e.clear(!1,t.backgroundColor||this._backgroundColor),t.pixelRatio<=this.dpr){this.refresh();var n=e.dom.width,i=e.dom.height,r=e.ctx;this.eachLayer(function(t){t.__builtin__?r.drawImage(t.dom,0,0,n,i):t.renderToCanvas&&(e.ctx.save(),t.renderToCanvas(e.ctx),e.ctx.restore())})}else for(var o={},a=this.storage.getDisplayList(!0),s=0;s<a.length;s++){var l=a[s];this._doPaintEl(l,e,!0,o)}return e.dom},getWidth:function(){return this._width},getHeight:function(){return this._height},_getSize:function(t){var e=this._opts,n=["width","height"][t],i=["clientWidth","clientHeight"][t],r=["paddingLeft","paddingTop"][t],o=["paddingRight","paddingBottom"][t];if(null!=e[n]&&"auto"!==e[n])return parseFloat(e[n]);var a=this.root,s=document.defaultView.getComputedStyle(a);return(a[i]||Ye(s[n])||Ye(a.style[n]))-(Ye(s[r])||0)-(Ye(s[o])||0)|0},pathToImage:function(t,e){e=e||this.dpr;var n=document.createElement("canvas"),i=n.getContext("2d"),r=t.getBoundingRect(),o=t.style,a=o.shadowBlur*e,s=o.shadowOffsetX*e,l=o.shadowOffsetY*e,u=o.hasStroke()?o.lineWidth:0,h=Math.max(u/2,-s+a),c=Math.max(u/2,s+a),d=Math.max(u/2,-l+a),f=Math.max(u/2,l+a),p=r.width+h+c,g=r.height+d+f;n.width=p*e,n.height=g*e,i.scale(e,e),i.clearRect(0,0,p,g),i.dpr=e;var m={position:t.position,rotation:t.rotation,scale:t.scale};t.position=[h-r.x,d-r.y],t.rotation=0,t.scale=[1,1],t.updateTransform(),t&&t.brush(i);var v=new je({style:{x:0,y:0,image:n}});return null!=m.position&&(v.position=t.position=m.position),null!=m.rotation&&(v.rotation=t.rotation=m.rotation),null!=m.scale&&(v.scale=t.scale=m.scale),v}};var Qg="undefined"!=typeof window&&!!window.addEventListener,Jg=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,tm=Qg?function(t){t.preventDefault(),t.stopPropagation(),t.cancelBubble=!0}:function(t){t.returnValue=!1,t.cancelBubble=!0},em=function(t){t=t||{},this.stage=t.stage||{},this.onframe=t.onframe||function(){},this._clips=[],this._running=!1,this._time,this._pausedTime,this._pauseStart,this._paused=!1,Zp.call(this)};em.prototype={constructor:em,addClip:function(t){this._clips.push(t)},addAnimator:function(t){t.animation=this;for(var e=t.getClips(),n=0;n<e.length;n++)this.addClip(e[n])},removeClip:function(t){var e=l(this._clips,t);e>=0&&this._clips.splice(e,1)},removeAnimator:function(t){for(var e=t.getClips(),n=0;n<e.length;n++)this.removeClip(e[n]);t.animation=null},_update:function(){for(var t=(new Date).getTime()-this._pausedTime,e=t-this._time,n=this._clips,i=n.length,r=[],o=[],a=0;a<i;a++){var s=n[a],l=s.step(t,e);l&&(r.push(l),o.push(s))}for(a=0;a<i;)n[a]._needsRemove?(n[a]=n[i-1],n.pop(),i--):a++;i=r.length;for(a=0;a<i;a++)o[a].fire(r[a]);this._time=t,this.onframe(e),this.trigger("frame",e),this.stage.update&&this.stage.update()},_startLoop:function(){function t(){e._running&&(Rg(t),!e._paused&&e._update())}var e=this;this._running=!0,Rg(t)},start:function(){this._time=(new Date).getTime(),this._pausedTime=0,this._startLoop()},stop:function(){this._running=!1},pause:function(){this._paused||(this._pauseStart=(new Date).getTime(),this._paused=!0)},resume:function(){this._paused&&(this._pausedTime+=(new Date).getTime()-this._pauseStart,this._paused=!1)},clear:function(){this._clips=[]},isFinished:function(){return!this._clips.length},animate:function(t,e){var n=new pg(t,(e=e||{}).loop,e.getter,e.setter);return this.addAnimator(n),n}},h(em,Zp);var nm=function(){this._track=[]};nm.prototype={constructor:nm,recognize:function(t,e,n){return this._doTrack(t,e,n),this._recognize(t)},clear:function(){return this._track.length=0,this},_doTrack:function(t,e,n){var i=t.touches;if(i){for(var r={points:[],touches:[],target:e,event:t},o=0,a=i.length;o<a;o++){var s=i[o],l=en(n,s,{});r.points.push([l.zrX,l.zrY]),r.touches.push(s)}this._track.push(r)}},_recognize:function(t){for(var e in im)if(im.hasOwnProperty(e)){var n=im[e](this._track,t);if(n)return n}}};var im={pinch:function(t,e){var n=t.length;if(n){var i=(t[n-1]||{}).points,r=(t[n-2]||{}).points||i;if(r&&r.length>1&&i&&i.length>1){var o=ln(i)/ln(r);!isFinite(o)&&(o=1),e.pinchScale=o;var a=un(i);return e.pinchX=a[0],e.pinchY=a[1],{type:"pinch",target:t[0].target,event:e}}}}},rm=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],om=["touchstart","touchend","touchmove"],am={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},sm=f(rm,function(t){var e=t.replace("mouse","pointer");return am[e]?e:t}),lm={mousemove:function(t){t=rn(this.dom,t),this.trigger("mousemove",t)},mouseout:function(t){var e=(t=rn(this.dom,t)).toElement||t.relatedTarget;if(e!=this.dom)for(;e&&9!=e.nodeType;){if(e===this.dom)return;e=e.parentNode}this.trigger("mouseout",t)},touchstart:function(t){(t=rn(this.dom,t)).zrByTouch=!0,this._lastTouchMoment=new Date,cn(this,t,"start"),lm.mousemove.call(this,t),lm.mousedown.call(this,t),dn(this)},touchmove:function(t){(t=rn(this.dom,t)).zrByTouch=!0,cn(this,t,"change"),lm.mousemove.call(this,t),dn(this)},touchend:function(t){(t=rn(this.dom,t)).zrByTouch=!0,cn(this,t,"end"),lm.mouseup.call(this,t),+new Date-this._lastTouchMoment<300&&lm.click.call(this,t),dn(this)},pointerdown:function(t){lm.mousedown.call(this,t)},pointermove:function(t){fn(t)||lm.mousemove.call(this,t)},pointerup:function(t){lm.mouseup.call(this,t)},pointerout:function(t){fn(t)||lm.mouseout.call(this,t)}};d(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],function(t){lm[t]=function(e){e=rn(this.dom,e),this.trigger(t,e)}});var um=gn.prototype;um.dispose=function(){for(var t=rm.concat(om),e=0;e<t.length;e++){var n=t[e];an(this.dom,hn(n),this._handlers[n])}},um.setCursor=function(t){this.dom.style&&(this.dom.style.cursor=t||"default")},h(gn,Zp);var hm=!bp.canvasSupported,cm={canvas:Kg},dm={},fm=function(t,e,n){n=n||{},this.dom=e,this.id=t;var i=this,r=new Tg,o=n.renderer;if(hm){if(!cm.vml)throw new Error("You need to require 'zrender/vml/vml' to support IE8");o="vml"}else o&&cm[o]||(o="canvas");var a=new cm[o](e,r,n,t);this.storage=r,this.painter=a;var s=bp.node||bp.worker?null:new gn(a.getViewportRoot());this.handler=new jp(r,a,s,a.root),this.animation=new em({stage:{update:m(this.flush,this)}}),this.animation.start(),this._needsRefresh;var l=r.delFromStorage,u=r.addToStorage;r.delFromStorage=function(t){l.call(r,t),t&&t.removeSelfFromZr(i)},r.addToStorage=function(t){u.call(r,t),t.addSelfToZr(i)}};fm.prototype={constructor:fm,getId:function(){return this.id},add:function(t){this.storage.addRoot(t),this._needsRefresh=!0},remove:function(t){this.storage.delRoot(t),this._needsRefresh=!0},configLayer:function(t,e){this.painter.configLayer&&this.painter.configLayer(t,e),this._needsRefresh=!0},setBackgroundColor:function(t){this.painter.setBackgroundColor&&this.painter.setBackgroundColor(t),this._needsRefresh=!0},refreshImmediately:function(){this._needsRefresh=!1,this.painter.refresh(),this._needsRefresh=!1},refresh:function(){this._needsRefresh=!0},flush:function(){var t;this._needsRefresh&&(t=!0,this.refreshImmediately()),this._needsRefreshHover&&(t=!0,this.refreshHoverImmediately()),t&&this.trigger("rendered")},addHover:function(t,e){this.painter.addHover&&(this.painter.addHover(t,e),this.refreshHover())},removeHover:function(t){this.painter.removeHover&&(this.painter.removeHover(t),this.refreshHover())},clearHover:function(){this.painter.clearHover&&(this.painter.clearHover(),this.refreshHover())},refreshHover:function(){this._needsRefreshHover=!0},refreshHoverImmediately:function(){this._needsRefreshHover=!1,this.painter.refreshHover&&this.painter.refreshHover()},resize:function(t){t=t||{},this.painter.resize(t.width,t.height),this.handler.resize()},clearAnimation:function(){this.animation.clear()},getWidth:function(){return this.painter.getWidth()},getHeight:function(){return this.painter.getHeight()},pathToImage:function(t,e){return this.painter.pathToImage(t,e)},setCursorStyle:function(t){this.handler.setCursorStyle(t)},findHover:function(t,e){return this.handler.findHover(t,e)},on:function(t,e,n){this.handler.on(t,e,n)},off:function(t,e){this.handler.off(t,e)},trigger:function(t,e){this.handler.trigger(t,e)},clear:function(){this.storage.delRoot(),this.painter.clear()},dispose:function(){this.animation.stop(),this.clear(),this.storage.dispose(),this.painter.dispose(),this.handler.dispose(),this.animation=this.storage=this.painter=this.handler=null,yn(this.id)}};var pm=(Object.freeze||Object)({version:"4.0.4",init:mn,dispose:function(t){if(t)t.dispose();else{for(var e in dm)dm.hasOwnProperty(e)&&dm[e].dispose();dm={}}return this},getInstance:function(t){return dm[t]},registerPainter:vn}),gm=d,mm=w,vm=y,ym="series\0",xm=["fontStyle","fontWeight","fontSize","fontFamily","rich","tag","color","textBorderColor","textBorderWidth","width","height","lineHeight","align","verticalAlign","baseline","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY","textShadowColor","textShadowBlur","textShadowOffsetX","textShadowOffsetY","backgroundColor","borderColor","borderWidth","borderRadius","padding"],_m=0,wm=".",bm="___EC__COMPONENT__CONTAINER___",Mm=0,Sm=function(t){for(var e=0;e<t.length;e++)t[e][1]||(t[e][1]=t[e][0]);return function(e,n,i){for(var r={},o=0;o<t.length;o++){var a=t[o][1];if(!(n&&l(n,a)>=0||i&&l(i,a)<0)){var s=e.getShallow(a);null!=s&&(r[t[o][0]]=s)}}return r}},Im=Sm([["lineWidth","width"],["stroke","color"],["opacity"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["shadowColor"]]),Cm={getLineStyle:function(t){var e=Im(this,t),n=this.getLineDash(e.lineWidth);return n&&(e.lineDash=n),e},getLineDash:function(t){null==t&&(t=1);var e=this.get("type"),n=Math.max(t,2),i=4*t;return"solid"===e||null==e?null:"dashed"===e?[i,i]:[n,n]}},Tm=Sm([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),Dm={getAreaStyle:function(t,e){return Tm(this,t,e)}},Am=Math.pow,km=Math.sqrt,Pm=1e-8,Lm=1e-4,Om=km(3),zm=1/3,Em=B(),Nm=B(),Rm=B(),Bm=Math.min,Vm=Math.max,Fm=Math.sin,Hm=Math.cos,Gm=2*Math.PI,Wm=B(),Zm=B(),Um=B(),Xm=[],jm=[],Ym={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},qm=[],$m=[],Km=[],Qm=[],Jm=Math.min,tv=Math.max,ev=Math.cos,nv=Math.sin,iv=Math.sqrt,rv=Math.abs,ov="undefined"!=typeof Float32Array,av=function(t){this._saveData=!t,this._saveData&&(this.data=[]),this._ctx=null};av.prototype={constructor:av,_xi:0,_yi:0,_x0:0,_y0:0,_ux:0,_uy:0,_len:0,_lineDash:null,_dashOffset:0,_dashIdx:0,_dashSum:0,setScale:function(t,e){this._ux=rv(1/mg/t)||0,this._uy=rv(1/mg/e)||0},getContext:function(){return this._ctx},beginPath:function(t){return this._ctx=t,t&&t.beginPath(),t&&(this.dpr=t.dpr),this._saveData&&(this._len=0),this._lineDash&&(this._lineDash=null,this._dashOffset=0),this},moveTo:function(t,e){return this.addData(Ym.M,t,e),this._ctx&&this._ctx.moveTo(t,e),this._x0=t,this._y0=e,this._xi=t,this._yi=e,this},lineTo:function(t,e){var n=rv(t-this._xi)>this._ux||rv(e-this._yi)>this._uy||this._len<5;return this.addData(Ym.L,t,e),this._ctx&&n&&(this._needsDash()?this._dashedLineTo(t,e):this._ctx.lineTo(t,e)),n&&(this._xi=t,this._yi=e),this},bezierCurveTo:function(t,e,n,i,r,o){return this.addData(Ym.C,t,e,n,i,r,o),this._ctx&&(this._needsDash()?this._dashedBezierTo(t,e,n,i,r,o):this._ctx.bezierCurveTo(t,e,n,i,r,o)),this._xi=r,this._yi=o,this},quadraticCurveTo:function(t,e,n,i){return this.addData(Ym.Q,t,e,n,i),this._ctx&&(this._needsDash()?this._dashedQuadraticTo(t,e,n,i):this._ctx.quadraticCurveTo(t,e,n,i)),this._xi=n,this._yi=i,this},arc:function(t,e,n,i,r,o){return this.addData(Ym.A,t,e,n,n,i,r-i,0,o?0:1),this._ctx&&this._ctx.arc(t,e,n,i,r,o),this._xi=ev(r)*n+t,this._yi=nv(r)*n+t,this},arcTo:function(t,e,n,i,r){return this._ctx&&this._ctx.arcTo(t,e,n,i,r),this},rect:function(t,e,n,i){return this._ctx&&this._ctx.rect(t,e,n,i),this.addData(Ym.R,t,e,n,i),this},closePath:function(){this.addData(Ym.Z);var t=this._ctx,e=this._x0,n=this._y0;return t&&(this._needsDash()&&this._dashedLineTo(e,n),t.closePath()),this._xi=e,this._yi=n,this},fill:function(t){t&&t.fill(),this.toStatic()},stroke:function(t){t&&t.stroke(),this.toStatic()},setLineDash:function(t){if(t instanceof Array){this._lineDash=t,this._dashIdx=0;for(var e=0,n=0;n<t.length;n++)e+=t[n];this._dashSum=e}return this},setLineDashOffset:function(t){return this._dashOffset=t,this},len:function(){return this._len},setData:function(t){var e=t.length;this.data&&this.data.length==e||!ov||(this.data=new Float32Array(e));for(var n=0;n<e;n++)this.data[n]=t[n];this._len=e},appendPath:function(t){t instanceof Array||(t=[t]);for(var e=t.length,n=0,i=this._len,r=0;r<e;r++)n+=t[r].len();ov&&this.data instanceof Float32Array&&(this.data=new Float32Array(i+n));for(r=0;r<e;r++)for(var o=t[r].data,a=0;a<o.length;a++)this.data[i++]=o[a];this._len=i},addData:function(t){if(this._saveData){var e=this.data;this._len+arguments.length>e.length&&(this._expandData(),e=this.data);for(var n=0;n<arguments.length;n++)e[this._len++]=arguments[n];this._prevCmd=t}},_expandData:function(){if(!(this.data instanceof Array)){for(var t=[],e=0;e<this._len;e++)t[e]=this.data[e];this.data=t}},_needsDash:function(){return this._lineDash},_dashedLineTo:function(t,e){var n,i,r=this._dashSum,o=this._dashOffset,a=this._lineDash,s=this._ctx,l=this._xi,u=this._yi,h=t-l,c=e-u,d=iv(h*h+c*c),f=l,p=u,g=a.length;for(h/=d,c/=d,o<0&&(o=r+o),f-=(o%=r)*h,p-=o*c;h>0&&f<=t||h<0&&f>=t||0==h&&(c>0&&p<=e||c<0&&p>=e);)f+=h*(n=a[i=this._dashIdx]),p+=c*n,this._dashIdx=(i+1)%g,h>0&&f<l||h<0&&f>l||c>0&&p<u||c<0&&p>u||s[i%2?"moveTo":"lineTo"](h>=0?Jm(f,t):tv(f,t),c>=0?Jm(p,e):tv(p,e));h=f-t,c=p-e,this._dashOffset=-iv(h*h+c*c)},_dashedBezierTo:function(t,e,n,i,r,o){var a,s,l,u,h,c=this._dashSum,d=this._dashOffset,f=this._lineDash,p=this._ctx,g=this._xi,m=this._yi,v=Gn,y=0,x=this._dashIdx,_=f.length,w=0;for(d<0&&(d=c+d),d%=c,a=0;a<1;a+=.1)s=v(g,t,n,r,a+.1)-v(g,t,n,r,a),l=v(m,e,i,o,a+.1)-v(m,e,i,o,a),y+=iv(s*s+l*l);for(;x<_&&!((w+=f[x])>d);x++);for(a=(w-d)/y;a<=1;)u=v(g,t,n,r,a),h=v(m,e,i,o,a),x%2?p.moveTo(u,h):p.lineTo(u,h),a+=f[x]/y,x=(x+1)%_;x%2!=0&&p.lineTo(r,o),s=r-u,l=o-h,this._dashOffset=-iv(s*s+l*l)},_dashedQuadraticTo:function(t,e,n,i){var r=n,o=i;n=(n+2*t)/3,i=(i+2*e)/3,t=(this._xi+2*t)/3,e=(this._yi+2*e)/3,this._dashedBezierTo(t,e,n,i,r,o)},toStatic:function(){var t=this.data;t instanceof Array&&(t.length=this._len,ov&&(this.data=new Float32Array(t)))},getBoundingRect:function(){qm[0]=qm[1]=Km[0]=Km[1]=Number.MAX_VALUE,$m[0]=$m[1]=Qm[0]=Qm[1]=-Number.MAX_VALUE;for(var t=this.data,e=0,n=0,i=0,r=0,o=0;o<t.length;){var a=t[o++];switch(1==o&&(i=e=t[o],r=n=t[o+1]),a){case Ym.M:e=i=t[o++],n=r=t[o++],Km[0]=i,Km[1]=r,Qm[0]=i,Qm[1]=r;break;case Ym.L:ei(e,n,t[o],t[o+1],Km,Qm),e=t[o++],n=t[o++];break;case Ym.C:ni(e,n,t[o++],t[o++],t[o++],t[o++],t[o],t[o+1],Km,Qm),e=t[o++],n=t[o++];break;case Ym.Q:ii(e,n,t[o++],t[o++],t[o],t[o+1],Km,Qm),e=t[o++],n=t[o++];break;case Ym.A:var s=t[o++],l=t[o++],u=t[o++],h=t[o++],c=t[o++],d=t[o++]+c,f=(t[o++],1-t[o++]);1==o&&(i=ev(c)*u+s,r=nv(c)*h+l),ri(s,l,u,h,c,d,f,Km,Qm),e=ev(d)*u+s,n=nv(d)*h+l;break;case Ym.R:ei(i=e=t[o++],r=n=t[o++],i+t[o++],r+t[o++],Km,Qm);break;case Ym.Z:e=i,n=r}K(qm,qm,Km),Q($m,$m,Qm)}return 0===o&&(qm[0]=qm[1]=$m[0]=$m[1]=0),new Xt(qm[0],qm[1],$m[0]-qm[0],$m[1]-qm[1])},rebuildPath:function(t){for(var e,n,i,r,o,a,s=this.data,l=this._ux,u=this._uy,h=this._len,c=0;c<h;){var d=s[c++];switch(1==c&&(e=i=s[c],n=r=s[c+1]),d){case Ym.M:e=i=s[c++],n=r=s[c++],t.moveTo(i,r);break;case Ym.L:o=s[c++],a=s[c++],(rv(o-i)>l||rv(a-r)>u||c===h-1)&&(t.lineTo(o,a),i=o,r=a);break;case Ym.C:t.bezierCurveTo(s[c++],s[c++],s[c++],s[c++],s[c++],s[c++]),i=s[c-2],r=s[c-1];break;case Ym.Q:t.quadraticCurveTo(s[c++],s[c++],s[c++],s[c++]),i=s[c-2],r=s[c-1];break;case Ym.A:var f=s[c++],p=s[c++],g=s[c++],m=s[c++],v=s[c++],y=s[c++],x=s[c++],_=s[c++],w=g>m?g:m,b=g>m?1:g/m,M=g>m?m/g:1,S=v+y;Math.abs(g-m)>.001?(t.translate(f,p),t.rotate(x),t.scale(b,M),t.arc(0,0,w,v,S,1-_),t.scale(1/b,1/M),t.rotate(-x),t.translate(-f,-p)):t.arc(f,p,w,v,S,1-_),1==c&&(e=ev(v)*g+f,n=nv(v)*m+p),i=ev(S)*g+f,r=nv(S)*m+p;break;case Ym.R:e=i=s[c],n=r=s[c+1],t.rect(s[c++],s[c++],s[c++],s[c++]);break;case Ym.Z:t.closePath(),i=e,r=n}}}},av.CMD=Ym;var sv=2*Math.PI,lv=2*Math.PI,uv=av.CMD,hv=2*Math.PI,cv=1e-4,dv=[-1,-1,-1],fv=[-1,-1],pv=Eg.prototype.getCanvasPattern,gv=Math.abs,mv=new av(!0);xi.prototype={constructor:xi,type:"path",__dirtyPath:!0,strokeContainThreshold:5,brush:function(t,e){var n=this.style,i=this.path||mv,r=n.hasStroke(),o=n.hasFill(),a=n.fill,s=n.stroke,l=o&&!!a.colorStops,u=r&&!!s.colorStops,h=o&&!!a.image,c=r&&!!s.image;if(n.bind(t,this,e),this.setTransform(t),this.__dirty){var d;l&&(d=d||this.getBoundingRect(),this._fillGradient=n.getGradient(t,a,d)),u&&(d=d||this.getBoundingRect(),this._strokeGradient=n.getGradient(t,s,d))}l?t.fillStyle=this._fillGradient:h&&(t.fillStyle=pv.call(a,t)),u?t.strokeStyle=this._strokeGradient:c&&(t.strokeStyle=pv.call(s,t));var f=n.lineDash,p=n.lineDashOffset,g=!!t.setLineDash,m=this.getGlobalScale();i.setScale(m[0],m[1]),this.__dirtyPath||f&&!g&&r?(i.beginPath(t),f&&!g&&(i.setLineDash(f),i.setLineDashOffset(p)),this.buildPath(i,this.shape,!1),this.path&&(this.__dirtyPath=!1)):(t.beginPath(),this.path.rebuildPath(t)),o&&i.fill(t),f&&g&&(t.setLineDash(f),t.lineDashOffset=p),r&&i.stroke(t),f&&g&&t.setLineDash([]),null!=n.text&&(this.restoreTransform(t),this.drawRectText(t,this.getBoundingRect()))},buildPath:function(t,e,n){},createPathProxy:function(){this.path=new av},getBoundingRect:function(){var t=this._rect,e=this.style,n=!t;if(n){var i=this.path;i||(i=this.path=new av),this.__dirtyPath&&(i.beginPath(),this.buildPath(i,this.shape,!1)),t=i.getBoundingRect()}if(this._rect=t,e.hasStroke()){var r=this._rectWithStroke||(this._rectWithStroke=t.clone());if(this.__dirty||n){r.copy(t);var o=e.lineWidth,a=e.strokeNoScale?this.getLineScale():1;e.hasFill()||(o=Math.max(o,this.strokeContainThreshold||4)),a>1e-10&&(r.width+=o/a,r.height+=o/a,r.x-=o/a/2,r.y-=o/a/2)}return r}return t},contain:function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect(),r=this.style;if(t=n[0],e=n[1],i.contain(t,e)){var o=this.path.data;if(r.hasStroke()){var a=r.lineWidth,s=r.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(r.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),yi(o,a/s,t,e)))return!0}if(r.hasFill())return vi(o,t,e)}return!1},dirty:function(t){null==t&&(t=!0),t&&(this.__dirtyPath=t,this._rect=null),this.__dirty=!0,this.__zr&&this.__zr.refresh(),this.__clipTarget&&this.__clipTarget.dirty()},animateShape:function(t){return this.animate("shape",t)},attrKV:function(t,e){"shape"===t?(this.setShape(e),this.__dirtyPath=!0,this._rect=null):Xe.prototype.attrKV.call(this,t,e)},setShape:function(t,e){var n=this.shape;if(n){if(w(t))for(var i in t)t.hasOwnProperty(i)&&(n[i]=t[i]);else n[t]=e;this.dirty(!0)}return this},getLineScale:function(){var t=this.transform;return t&&gv(t[0]-1)>1e-10&&gv(t[3]-1)>1e-10?Math.sqrt(gv(t[0]*t[3]-t[2]*t[1])):1}},xi.extend=function(t){var e=function(e){xi.call(this,e),t.style&&this.style.extendFrom(t.style,!1);var n=t.shape;if(n){this.shape=this.shape||{};var i=this.shape;for(var r in n)!i.hasOwnProperty(r)&&n.hasOwnProperty(r)&&(i[r]=n[r])}t.init&&t.init.call(this,e)};u(e,xi);for(var n in t)"style"!==n&&"shape"!==n&&(e.prototype[n]=t[n]);return e},u(xi,Xe);var vv=av.CMD,yv=[[],[],[]],xv=Math.sqrt,_v=Math.atan2,wv=function(t,e){var n,i,r,o,a,s,l=t.data,u=vv.M,h=vv.C,c=vv.L,d=vv.R,f=vv.A,p=vv.Q;for(r=0,o=0;r<l.length;){switch(n=l[r++],o=r,i=0,n){case u:case c:i=1;break;case h:i=3;break;case p:i=2;break;case f:var g=e[4],m=e[5],v=xv(e[0]*e[0]+e[1]*e[1]),y=xv(e[2]*e[2]+e[3]*e[3]),x=_v(-e[1]/y,e[0]/v);l[r]*=v,l[r++]+=g,l[r]*=y,l[r++]+=m,l[r++]*=v,l[r++]*=y,l[r++]+=x,l[r++]+=x,o=r+=2;break;case d:s[0]=l[r++],s[1]=l[r++],$(s,s,e),l[o++]=s[0],l[o++]=s[1],s[0]+=l[r++],s[1]+=l[r++],$(s,s,e),l[o++]=s[0],l[o++]=s[1]}for(a=0;a<i;a++)(s=yv[a])[0]=l[r++],s[1]=l[r++],$(s,s,e),l[o++]=s[0],l[o++]=s[1]}},bv=["m","M","l","L","v","V","h","H","z","Z","c","C","q","Q","t","T","s","S","a","A"],Mv=Math.sqrt,Sv=Math.sin,Iv=Math.cos,Cv=Math.PI,Tv=function(t){return Math.sqrt(t[0]*t[0]+t[1]*t[1])},Dv=function(t,e){return(t[0]*e[0]+t[1]*e[1])/(Tv(t)*Tv(e))},Av=function(t,e){return(t[0]*e[1]<t[1]*e[0]?-1:1)*Math.acos(Dv(t,e))},kv=function(t){Xe.call(this,t)};kv.prototype={constructor:kv,type:"text",brush:function(t,e){var n=this.style;this.__dirty&&De(n),n.fill=n.stroke=n.shadowBlur=n.shadowColor=n.shadowOffsetX=n.shadowOffsetY=null;var i=n.text;null!=i&&(i+=""),n.bind(t,this,e),Ue(i,n)&&(this.setTransform(t),ke(this,t,i,n),this.restoreTransform(t))},getBoundingRect:function(){var t=this.style;if(this.__dirty&&De(t),!this._rect){var e=t.text;null!=e?e+="":e="";var n=ce(t.text+"",t.font,t.textAlign,t.textVerticalAlign,t.textPadding,t.rich);if(n.x+=t.x||0,n.y+=t.y||0,He(t.textStroke,t.textStrokeWidth)){var i=t.textStrokeWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect}},u(kv,Xe);var Pv=xi.extend({type:"circle",shape:{cx:0,cy:0,r:0},buildPath:function(t,e,n){n&&t.moveTo(e.cx+e.r,e.cy),t.arc(e.cx,e.cy,e.r,0,2*Math.PI,!0)}}),Lv=[["shadowBlur",0],["shadowColor","#000"],["shadowOffsetX",0],["shadowOffsetY",0]],Ov=function(t){return bp.browser.ie&&bp.browser.version>=11?function(){var e,n=this.__clipPaths,i=this.style;if(n)for(var r=0;r<n.length;r++){var o=n[r],a=o&&o.shape,s=o&&o.type;if(a&&("sector"===s&&a.startAngle===a.endAngle||"rect"===s&&(!a.width||!a.height))){for(l=0;l<Lv.length;l++)Lv[l][2]=i[Lv[l][0]],i[Lv[l][0]]=Lv[l][1];e=!0;break}}if(t.apply(this,arguments),e)for(var l=0;l<Lv.length;l++)i[Lv[l][0]]=Lv[l][2]}:t},zv=xi.extend({type:"sector",shape:{cx:0,cy:0,r0:0,r:0,startAngle:0,endAngle:2*Math.PI,clockwise:!0},brush:Ov(xi.prototype.brush),buildPath:function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r0||0,0),o=Math.max(e.r,0),a=e.startAngle,s=e.endAngle,l=e.clockwise,u=Math.cos(a),h=Math.sin(a);t.moveTo(u*r+n,h*r+i),t.lineTo(u*o+n,h*o+i),t.arc(n,i,o,a,s,!l),t.lineTo(Math.cos(s)*r+n,Math.sin(s)*r+i),0!==r&&t.arc(n,i,r,s,a,l),t.closePath()}}),Ev=xi.extend({type:"ring",shape:{cx:0,cy:0,r:0,r0:0},buildPath:function(t,e){var n=e.cx,i=e.cy,r=2*Math.PI;t.moveTo(n+e.r,i),t.arc(n,i,e.r,0,r,!1),t.moveTo(n+e.r0,i),t.arc(n,i,e.r0,0,r,!0)}}),Nv=function(t,e){for(var n=t.length,i=[],r=0,o=1;o<n;o++)r+=Y(t[o-1],t[o]);var a=r/2;a=a<n?n:a;for(o=0;o<a;o++){var s,l,u,h=o/(a-1)*(e?n:n-1),c=Math.floor(h),d=h-c,f=t[c%n];e?(s=t[(c-1+n)%n],l=t[(c+1)%n],u=t[(c+2)%n]):(s=t[0===c?c:c-1],l=t[c>n-2?n-1:c+1],u=t[c>n-3?n-1:c+2]);var p=d*d,g=d*p;i.push([Ii(s[0],f[0],l[0],u[0],d,p,g),Ii(s[1],f[1],l[1],u[1],d,p,g)])}return i},Rv=function(t,e,n,i){var r,o,a,s,l=[],u=[],h=[],c=[];if(i){a=[1/0,1/0],s=[-1/0,-1/0];for(var d=0,f=t.length;d<f;d++)K(a,a,t[d]),Q(s,s,t[d]);K(a,a,i[0]),Q(s,s,i[1])}for(var d=0,f=t.length;d<f;d++){var p=t[d];if(n)r=t[d?d-1:f-1],o=t[(d+1)%f];else{if(0===d||d===f-1){l.push(F(t[d]));continue}r=t[d-1],o=t[d+1]}W(u,o,r),X(u,u,e);var g=Y(p,r),m=Y(p,o),v=g+m;0!==v&&(g/=v,m/=v),X(h,u,-g),X(c,u,m);var y=H([],p,h),x=H([],p,c);i&&(Q(y,y,a),K(y,y,s),Q(x,x,a),K(x,x,s)),l.push(y),l.push(x)}return n&&l.push(l.shift()),l},Bv=xi.extend({type:"polygon",shape:{points:null,smooth:!1,smoothConstraint:null},buildPath:function(t,e){Ci(t,e,!0)}}),Vv=xi.extend({type:"polyline",shape:{points:null,smooth:!1,smoothConstraint:null},style:{stroke:"#000",fill:null},buildPath:function(t,e){Ci(t,e,!1)}}),Fv=xi.extend({type:"rect",shape:{r:0,x:0,y:0,width:0,height:0},buildPath:function(t,e){var n=e.x,i=e.y,r=e.width,o=e.height;e.r?Te(t,e):t.rect(n,i,r,o),t.closePath()}}),Hv=xi.extend({type:"line",shape:{x1:0,y1:0,x2:0,y2:0,percent:1},style:{stroke:"#000",fill:null},buildPath:function(t,e){var n=e.x1,i=e.y1,r=e.x2,o=e.y2,a=e.percent;0!==a&&(t.moveTo(n,i),a<1&&(r=n*(1-a)+r*a,o=i*(1-a)+o*a),t.lineTo(r,o))},pointAt:function(t){var e=this.shape;return[e.x1*(1-t)+e.x2*t,e.y1*(1-t)+e.y2*t]}}),Gv=[],Wv=xi.extend({type:"bezier-curve",shape:{x1:0,y1:0,x2:0,y2:0,cpx1:0,cpy1:0,percent:1},style:{stroke:"#000",fill:null},buildPath:function(t,e){var n=e.x1,i=e.y1,r=e.x2,o=e.y2,a=e.cpx1,s=e.cpy1,l=e.cpx2,u=e.cpy2,h=e.percent;0!==h&&(t.moveTo(n,i),null==l||null==u?(h<1&&(Qn(n,a,r,h,Gv),a=Gv[1],r=Gv[2],Qn(i,s,o,h,Gv),s=Gv[1],o=Gv[2]),t.quadraticCurveTo(a,s,r,o)):(h<1&&(Xn(n,a,l,r,h,Gv),a=Gv[1],l=Gv[2],r=Gv[3],Xn(i,s,u,o,h,Gv),s=Gv[1],u=Gv[2],o=Gv[3]),t.bezierCurveTo(a,s,l,u,r,o)))},pointAt:function(t){return Ti(this.shape,t,!1)},tangentAt:function(t){var e=Ti(this.shape,t,!0);return j(e,e)}}),Zv=xi.extend({type:"arc",shape:{cx:0,cy:0,r:0,startAngle:0,endAngle:2*Math.PI,clockwise:!0},style:{stroke:"#000",fill:null},buildPath:function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r,0),o=e.startAngle,a=e.endAngle,s=e.clockwise,l=Math.cos(o),u=Math.sin(o);t.moveTo(l*r+n,u*r+i),t.arc(n,i,r,o,a,!s)}}),Uv=xi.extend({type:"compound",shape:{paths:null},_updatePathDirty:function(){for(var t=this.__dirtyPath,e=this.shape.paths,n=0;n<e.length;n++)t=t||e[n].__dirtyPath;this.__dirtyPath=t,this.__dirty=this.__dirty||t},beforeBrush:function(){this._updatePathDirty();for(var t=this.shape.paths||[],e=this.getGlobalScale(),n=0;n<t.length;n++)t[n].path||t[n].createPathProxy(),t[n].path.setScale(e[0],e[1])},buildPath:function(t,e){for(var n=e.paths||[],i=0;i<n.length;i++)n[i].buildPath(t,n[i].shape,!0)},afterBrush:function(){for(var t=this.shape.paths||[],e=0;e<t.length;e++)t[e].__dirtyPath=!1},getBoundingRect:function(){return this._updatePathDirty(),xi.prototype.getBoundingRect.call(this)}}),Xv=function(t){this.colorStops=t||[]};Xv.prototype={constructor:Xv,addColorStop:function(t,e){this.colorStops.push({offset:t,color:e})}};var jv=function(t,e,n,i,r,o){this.x=null==t?0:t,this.y=null==e?0:e,this.x2=null==n?1:n,this.y2=null==i?0:i,this.type="linear",this.global=o||!1,Xv.call(this,r)};jv.prototype={constructor:jv},u(jv,Xv);var Yv=function(t,e,n,i,r){this.x=null==t?.5:t,this.y=null==e?.5:e,this.r=null==n?.5:n,this.type="radial",this.global=r||!1,Xv.call(this,i)};Yv.prototype={constructor:Yv},u(Yv,Xv),Di.prototype.incremental=!0,Di.prototype.clearDisplaybles=function(){this._displayables=[],this._temporaryDisplayables=[],this._cursor=0,this.dirty(),this.notClear=!1},Di.prototype.addDisplayable=function(t,e){e?this._temporaryDisplayables.push(t):this._displayables.push(t),this.dirty()},Di.prototype.addDisplayables=function(t,e){e=e||!1;for(var n=0;n<t.length;n++)this.addDisplayable(t[n],e)},Di.prototype.eachPendingDisplayable=function(t){for(e=this._cursor;e<this._displayables.length;e++)t&&t(this._displayables[e]);for(var e=0;e<this._temporaryDisplayables.length;e++)t&&t(this._temporaryDisplayables[e])},Di.prototype.update=function(){this.updateTransform();for(t=this._cursor;t<this._displayables.length;t++)(e=this._displayables[t]).parent=this,e.update(),e.parent=null;for(var t=0;t<this._temporaryDisplayables.length;t++){var e=this._temporaryDisplayables[t];e.parent=this,e.update(),e.parent=null}},Di.prototype.brush=function(t,e){for(n=this._cursor;n<this._displayables.length;n++)(i=this._displayables[n]).beforeBrush&&i.beforeBrush(t),i.brush(t,n===this._cursor?null:this._displayables[n-1]),i.afterBrush&&i.afterBrush(t);this._cursor=n;for(var n=0;n<this._temporaryDisplayables.length;n++){var i=this._temporaryDisplayables[n];i.beforeBrush&&i.beforeBrush(t),i.brush(t,0===n?null:this._temporaryDisplayables[n-1]),i.afterBrush&&i.afterBrush(t)}this._temporaryDisplayables=[],this.notClear=!0};var qv=[];Di.prototype.getBoundingRect=function(){if(!this._rect){for(var t=new Xt(1/0,1/0,-1/0,-1/0),e=0;e<this._displayables.length;e++){var n=this._displayables[e],i=n.getBoundingRect().clone();n.needLocalTransform()&&i.applyTransform(n.getLocalTransform(qv)),t.union(i)}this._rect=t}return this._rect},Di.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e);if(this.getBoundingRect().contain(n[0],n[1]))for(var i=0;i<this._displayables.length;i++)if(this._displayables[i].contain(t,e))return!0;return!1},u(Di,Xe);var $v=Math.round,Kv=Math.max,Qv=Math.min,Jv={},ty=(Object.freeze||Object)({extendShape:Ai,extendPath:function(t,e){return Si(t,e)},makePath:ki,makeImage:Pi,mergePath:function(t,e){for(var n=[],i=t.length,r=0;r<i;r++){var o=t[r];o.path||o.createPathProxy(),o.__dirtyPath&&o.buildPath(o.path,o.shape,!0),n.push(o.path)}var a=new xi(e);return a.createPathProxy(),a.buildPath=function(t){t.appendPath(n);var e=t.getContext();e&&t.rebuildPath(e)},a},resizePath:Oi,subPixelOptimizeLine:zi,subPixelOptimizeRect:Ei,subPixelOptimize:Ni,setHoverStyle:qi,setLabelStyle:$i,setTextStyle:Ki,setText:function(t,e,n){var i,r={isRectText:!0};!1===n?i=!0:r.autoColor=n,Qi(t,e,r,i),t.host&&t.host.dirty&&t.host.dirty(!1)},getFont:rr,updateProps:ar,initProps:sr,getTransform:lr,applyTransform:ur,transformDirection:hr,groupTransition:cr,clipPointsByRect:dr,clipRectByRect:function(t,e){var n=Kv(t.x,e.x),i=Qv(t.x+t.width,e.x+e.width),r=Kv(t.y,e.y),o=Qv(t.y+t.height,e.y+e.height);if(i>=n&&o>=r)return{x:n,y:r,width:i-n,height:o-r}},createIcon:fr,Group:Sg,Image:je,Text:kv,Circle:Pv,Sector:zv,Ring:Ev,Polygon:Bv,Polyline:Vv,Rect:Fv,Line:Hv,BezierCurve:Wv,Arc:Zv,IncrementalDisplayable:Di,CompoundPath:Uv,LinearGradient:jv,RadialGradient:Yv,BoundingRect:Xt}),ey=["textStyle","color"],ny={getTextColor:function(t){var e=this.ecModel;return this.getShallow("color")||(!t&&e?e.get(ey):null)},getFont:function(){return rr({fontStyle:this.getShallow("fontStyle"),fontWeight:this.getShallow("fontWeight"),fontSize:this.getShallow("fontSize"),fontFamily:this.getShallow("fontFamily")},this.ecModel)},getTextRect:function(t){return ce(t,this.getFont(),this.getShallow("align"),this.getShallow("verticalAlign")||this.getShallow("baseline"),this.getShallow("padding"),this.getShallow("rich"),this.getShallow("truncateText"))}},iy=Sm([["fill","color"],["stroke","borderColor"],["lineWidth","borderWidth"],["opacity"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["shadowColor"],["textPosition"],["textAlign"]]),ry={getItemStyle:function(t,e){var n=iy(this,t,e),i=this.getBorderLineDash();return i&&(n.lineDash=i),n},getBorderLineDash:function(){var t=this.get("borderType");return"solid"===t||null==t?null:"dashed"===t?[5,5]:[1,1]}},oy=h,ay=Dn();pr.prototype={constructor:pr,init:null,mergeOption:function(t){i(this.option,t,!0)},get:function(t,e){return null==t?this.option:gr(this.option,this.parsePath(t),!e&&mr(this,t))},getShallow:function(t,e){var n=this.option,i=null==n?n:n[t],r=!e&&mr(this,t);return null==i&&r&&(i=r.getShallow(t)),i},getModel:function(t,e){var n,i=null==t?this.option:gr(this.option,t=this.parsePath(t));return e=e||(n=mr(this,t))&&n.getModel(t),new pr(i,e,this.ecModel)},isEmpty:function(){return null==this.option},restoreData:function(){},clone:function(){return new(0,this.constructor)(n(this.option))},setReadOnly:function(t){},parsePath:function(t){return"string"==typeof t&&(t=t.split(".")),t},customizeGetParent:function(t){ay(this).getParent=t},isAnimationEnabled:function(){if(!bp.node){if(null!=this.option.animation)return!!this.option.animation;if(this.parentModel)return this.parentModel.isAnimationEnabled()}}},En(pr),Nn(pr),oy(pr,Cm),oy(pr,Dm),oy(pr,ny),oy(pr,ry);var sy=0,ly=1e-4,uy=/^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d\d)(?::(\d\d)(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/,hy=(Object.freeze||Object)({linearMap:xr,parsePercent:_r,round:wr,asc:br,getPrecision:Mr,getPrecisionSafe:Sr,getPixelPrecision:Ir,getPercentWithPrecision:Cr,MAX_SAFE_INTEGER:9007199254740991,remRadian:Tr,isRadianAroundZero:Dr,parseDate:Ar,quantity:kr,nice:Lr,reformIntervals:function(t){function e(t,n,i){return t.interval[i]<n.interval[i]||t.interval[i]===n.interval[i]&&(t.close[i]-n.close[i]==(i?-1:1)||!i&&e(t,n,1))}t.sort(function(t,n){return e(t,n,0)?-1:1});for(var n=-1/0,i=1,r=0;r<t.length;){for(var o=t[r].interval,a=t[r].close,s=0;s<2;s++)o[s]<=n&&(o[s]=n,a[s]=s?1:1-i),n=o[s],i=a[s];o[0]===o[1]&&a[0]*a[1]!=1?t.splice(r,1):r++}return t},isNumeric:function(t){return t-parseFloat(t)>=0}}),cy=k,dy=/([&<>"'])/g,fy={"&":"&","<":"<",">":">",'"':""","'":"'"},py=["a","b","c","d","e","f","g"],gy=function(t,e){return"{"+t+(null==e?"":e)+"}"},my=ve,vy=ce,yy=(Object.freeze||Object)({addCommas:Or,toCamelCase:zr,normalizeCssArray:cy,encodeHTML:Er,formatTpl:Nr,formatTplSimple:function(t,e,n){return d(e,function(e,i){t=t.replace("{"+i+"}",n?Er(e):e)}),t},getTooltipMarker:Rr,formatTime:Vr,capitalFirst:Fr,truncateText:my,getTextRect:vy}),xy=d,_y=["left","right","top","bottom","width","height"],wy=[["width","left","right"],["height","top","bottom"]],by=Hr,My=(v(Hr,"vertical"),v(Hr,"horizontal"),{getBoxLayoutParams:function(){return{left:this.get("left"),top:this.get("top"),right:this.get("right"),bottom:this.get("bottom"),width:this.get("width"),height:this.get("height")}}}),Sy=Dn(),Iy=pr.extend({type:"component",id:"",name:"",mainType:"",subType:"",componentIndex:0,defaultOption:null,ecModel:null,dependentModels:[],uid:null,layoutMode:null,$constructor:function(t,e,n,i){pr.call(this,t,e,n,i),this.uid=vr("ec_cpt_model")},init:function(t,e,n,i){this.mergeDefaultAndTheme(t,n)},mergeDefaultAndTheme:function(t,e){var n=this.layoutMode,r=n?Ur(t):{};i(t,e.getTheme().get(this.mainType)),i(t,this.getDefaultOption()),n&&Zr(t,r,n)},mergeOption:function(t,e){i(this.option,t,!0);var n=this.layoutMode;n&&Zr(this.option,t,n)},optionUpdated:function(t,e){},getDefaultOption:function(){var t=Sy(this);if(!t.defaultOption){for(var e=[],n=this.constructor;n;){var r=n.prototype.defaultOption;r&&e.push(r),n=n.superClass}for(var o={},a=e.length-1;a>=0;a--)o=i(o,e[a],!0);t.defaultOption=o}return t.defaultOption},getReferringComponents:function(t){return this.ecModel.queryComponents({mainType:t,index:this.get(t+"Index",!0),id:this.get(t+"Id",!0)})}});Vn(Iy,{registerWhenExtend:!0}),function(t){var e={};t.registerSubTypeDefaulter=function(t,n){t=On(t),e[t.main]=n},t.determineSubType=function(n,i){var r=i.type;if(!r){var o=On(n).main;t.hasSubTypes(n)&&e[o]&&(r=e[o](i))}return r}}(Iy),function(t,e){function n(t){var n={},o=[];return d(t,function(a){var s=i(n,a),u=r(s.originalDeps=e(a),t);s.entryCount=u.length,0===s.entryCount&&o.push(a),d(u,function(t){l(s.predecessor,t)<0&&s.predecessor.push(t);var e=i(n,t);l(e.successor,t)<0&&e.successor.push(a)})}),{graph:n,noEntryList:o}}function i(t,e){return t[e]||(t[e]={predecessor:[],successor:[]}),t[e]}function r(t,e){var n=[];return d(t,function(t){l(e,t)>=0&&n.push(t)}),n}t.topologicalTravel=function(t,e,i,r){function o(t){s[t].entryCount--,0===s[t].entryCount&&l.push(t)}if(t.length){var a=n(e),s=a.graph,l=a.noEntryList,u={};for(d(t,function(t){u[t]=!0});l.length;){var h=l.pop(),c=s[h],f=!!u[h];f&&(i.call(r,h,c.originalDeps.slice()),delete u[h]),d(c.successor,f?function(t){u[t]=!0,o(t)}:o)}d(u,function(){throw new Error("Circle dependency may exists")})}}}(Iy,function(t){var e=[];return d(Iy.getClassesByMainType(t),function(t){e=e.concat(t.prototype.dependencies||[])}),e=f(e,function(t){return On(t).main}),"dataset"!==t&&l(e,"dataset")<=0&&e.unshift("dataset"),e}),h(Iy,My);var Cy="";"undefined"!=typeof navigator&&(Cy=navigator.platform||"");var Ty={color:["#c23531","#2f4554","#61a0a8","#d48265","#91c7ae","#749f83","#ca8622","#bda29a","#6e7074","#546570","#c4ccd3"],gradientColor:["#f6efa6","#d88273","#bf444c"],textStyle:{fontFamily:Cy.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,animation:"auto",animationDuration:1e3,animationDurationUpdate:300,animationEasing:"exponentialOut",animationEasingUpdate:"cubicOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},Dy=Dn(),Ay={clearColorPalette:function(){Dy(this).colorIdx=0,Dy(this).colorNameMap={}},getColorFromPalette:function(t,e,n){var i=Dy(e=e||this),r=i.colorIdx||0,o=i.colorNameMap=i.colorNameMap||{};if(o.hasOwnProperty(t))return o[t];var a=xn(this.get("color",!0)),s=this.get("colorLayer",!0),l=null!=n&&s?jr(s,n):a;if((l=l||a)&&l.length){var u=l[r];return t&&(o[t]=u),i.colorIdx=(r+1)%l.length,u}}},ky={cartesian2d:function(t,e,n,i){var r=t.getReferringComponents("xAxis")[0],o=t.getReferringComponents("yAxis")[0];e.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),qr(r)&&(i.set("x",r),e.firstCategoryDimIndex=0),qr(o)&&(i.set("y",o),e.firstCategoryDimIndex=1)},singleAxis:function(t,e,n,i){var r=t.getReferringComponents("singleAxis")[0];e.coordSysDims=["single"],n.set("single",r),qr(r)&&(i.set("single",r),e.firstCategoryDimIndex=0)},polar:function(t,e,n,i){var r=t.getReferringComponents("polar")[0],o=r.findAxisModel("radiusAxis"),a=r.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",a),qr(o)&&(i.set("radius",o),e.firstCategoryDimIndex=0),qr(a)&&(i.set("angle",a),e.firstCategoryDimIndex=1)},geo:function(t,e,n,i){e.coordSysDims=["lng","lat"]},parallel:function(t,e,n,i){var r=t.ecModel,o=r.getComponent("parallel",t.get("parallelIndex")),a=e.coordSysDims=o.dimensions.slice();d(o.parallelAxisIndex,function(t,o){var s=r.getComponent("parallelAxis",t),l=a[o];n.set(l,s),qr(s)&&null==e.firstCategoryDimIndex&&(i.set(l,s),e.firstCategoryDimIndex=o)})}},Py="original",Ly="arrayRows",Oy="objectRows",zy="keyedColumns",Ey="unknown",Ny="typedArray",Ry="column",By="row";$r.seriesDataToSource=function(t){return new $r({data:t,sourceFormat:M(t)?Ny:Py,fromDataset:!1})},Nn($r);var Vy=Dn(),Fy="\0_ec_inner",Hy=pr.extend({init:function(t,e,n,i){n=n||{},this.option=null,this._theme=new pr(n),this._optionManager=i},setOption:function(t,e){P(!(Fy in t),"please use chart.getOption()"),this._optionManager.setOption(t,e),this.resetOption(null)},resetOption:function(t){var e=!1,n=this._optionManager;if(!t||"recreate"===t){var i=n.mountOption("recreate"===t);this.option&&"recreate"!==t?(this.restoreData(),this.mergeOption(i)):co.call(this,i),e=!0}if("timeline"!==t&&"media"!==t||this.restoreData(),!t||"recreate"===t||"timeline"===t){var r=n.getTimelineOption(this);r&&(this.mergeOption(r),e=!0)}if(!t||"recreate"===t||"media"===t){var o=n.getMediaOption(this,this._api);o.length&&d(o,function(t){this.mergeOption(t,e=!0)},this)}return e},mergeOption:function(t){var e=this.option,r=this._componentsMap,a=[];Jr(this),d(t,function(t,r){null!=t&&(Iy.hasClass(r)?r&&a.push(r):e[r]=null==e[r]?n(t):i(e[r],t,!0))}),Iy.topologicalTravel(a,Iy.getAllClassMainTypes(),function(n,i){var a=xn(t[n]),s=Mn(r.get(n),a);Sn(s),d(s,function(t,e){var i=t.option;w(i)&&(t.keyInfo.mainType=n,t.keyInfo.subType=po(n,i,t.exist))});var l=fo(r,i);e[n]=[],r.set(n,[]),d(s,function(t,i){var a=t.exist,s=t.option;if(P(w(s)||a,"Empty component definition"),s){var u=Iy.getClass(n,t.keyInfo.subType,!0);if(a&&a instanceof u)a.name=t.keyInfo.name,a.mergeOption(s,this),a.optionUpdated(s,!1);else{var h=o({dependentModels:l,componentIndex:i},t.keyInfo);o(a=new u(s,this,this,h),h),a.init(s,this,this,h),a.optionUpdated(null,!0)}}else a.mergeOption({},this),a.optionUpdated({},!1);r.get(n)[i]=a,e[n][i]=a.option},this),"series"===n&&go(this,r.get("series"))},this),this._seriesIndicesMap=N(this._seriesIndices=this._seriesIndices||[])},getOption:function(){var t=n(this.option);return d(t,function(e,n){if(Iy.hasClass(n)){for(var i=(e=xn(e)).length-1;i>=0;i--)Cn(e[i])&&e.splice(i,1);t[n]=e}}),delete t[Fy],t},getTheme:function(){return this._theme},getComponent:function(t,e){var n=this._componentsMap.get(t);if(n)return n[e||0]},queryComponents:function(t){var e=t.mainType;if(!e)return[];var n=t.index,i=t.id,r=t.name,o=this._componentsMap.get(e);if(!o||!o.length)return[];var a;if(null!=n)y(n)||(n=[n]),a=g(f(n,function(t){return o[t]}),function(t){return!!t});else if(null!=i){var s=y(i);a=g(o,function(t){return s&&l(i,t.id)>=0||!s&&t.id===i})}else if(null!=r){var u=y(r);a=g(o,function(t){return u&&l(r,t.name)>=0||!u&&t.name===r})}else a=o.slice();return mo(a,t)},findComponents:function(t){var e=t.query,n=t.mainType,i=function(t){var e=n+"Index",i=n+"Id",r=n+"Name";return!t||null==t[e]&&null==t[i]&&null==t[r]?null:{mainType:n,index:t[e],id:t[i],name:t[r]}}(e);return function(e){return t.filter?g(e,t.filter):e}(mo(i?this.queryComponents(i):this._componentsMap.get(n),t))},eachComponent:function(t,e,n){var i=this._componentsMap;"function"==typeof t?(n=e,e=t,i.each(function(t,i){d(t,function(t,r){e.call(n,i,t,r)})})):_(t)?d(i.get(t),e,n):w(t)&&d(this.findComponents(t),e,n)},getSeriesByName:function(t){return g(this._componentsMap.get("series"),function(e){return e.name===t})},getSeriesByIndex:function(t){return this._componentsMap.get("series")[t]},getSeriesByType:function(t){return g(this._componentsMap.get("series"),function(e){return e.subType===t})},getSeries:function(){return this._componentsMap.get("series").slice()},getSeriesCount:function(){return this._componentsMap.get("series").length},eachSeries:function(t,e){d(this._seriesIndices,function(n){var i=this._componentsMap.get("series")[n];t.call(e,i,n)},this)},eachRawSeries:function(t,e){d(this._componentsMap.get("series"),t,e)},eachSeriesByType:function(t,e,n){d(this._seriesIndices,function(i){var r=this._componentsMap.get("series")[i];r.subType===t&&e.call(n,r,i)},this)},eachRawSeriesByType:function(t,e,n){return d(this.getSeriesByType(t),e,n)},isSeriesFiltered:function(t){return null==this._seriesIndicesMap.get(t.componentIndex)},getCurrentSeriesIndices:function(){return(this._seriesIndices||[]).slice()},filterSeries:function(t,e){go(this,g(this._componentsMap.get("series"),t,e))},restoreData:function(t){var e=this._componentsMap;go(this,e.get("series"));var n=[];e.each(function(t,e){n.push(e)}),Iy.topologicalTravel(n,Iy.getAllClassMainTypes(),function(n,i){d(e.get(n),function(e){("series"!==n||!uo(e,t))&&e.restoreData()})})}});h(Hy,Ay);var Gy=["getDom","getZr","getWidth","getHeight","getDevicePixelRatio","dispatchAction","isDisposed","on","off","getDataURL","getConnectedDataURL","getModel","getOption","getViewOfComponentModel","getViewOfSeriesModel"],Wy={};yo.prototype={constructor:yo,create:function(t,e){var n=[];d(Wy,function(i,r){var o=i.create(t,e);n=n.concat(o||[])}),this._coordinateSystems=n},update:function(t,e){d(this._coordinateSystems,function(n){n.update&&n.update(t,e)})},getCoordinateSystems:function(){return this._coordinateSystems.slice()}},yo.register=function(t,e){Wy[t]=e},yo.get=function(t){return Wy[t]};var Zy=d,Uy=n,Xy=f,jy=i,Yy=/^(min|max)?(.+)$/;xo.prototype={constructor:xo,setOption:function(t,e){t&&d(xn(t.series),function(t){t&&t.data&&M(t.data)&&O(t.data)}),t=Uy(t,!0);var n=this._optionBackup,i=_o.call(this,t,e,!n);this._newBaseOption=i.baseOption,n?(So(n.baseOption,i.baseOption),i.timelineOptions.length&&(n.timelineOptions=i.timelineOptions),i.mediaList.length&&(n.mediaList=i.mediaList),i.mediaDefault&&(n.mediaDefault=i.mediaDefault)):this._optionBackup=i},mountOption:function(t){var e=this._optionBackup;return this._timelineOptions=Xy(e.timelineOptions,Uy),this._mediaList=Xy(e.mediaList,Uy),this._mediaDefault=Uy(e.mediaDefault),this._currentMediaIndices=[],Uy(t?e.baseOption:this._newBaseOption)},getTimelineOption:function(t){var e,n=this._timelineOptions;if(n.length){var i=t.getComponent("timeline");i&&(e=Uy(n[i.getCurrentIndex()],!0))}return e},getMediaOption:function(t){var e=this._api.getWidth(),n=this._api.getHeight(),i=this._mediaList,r=this._mediaDefault,o=[],a=[];if(!i.length&&!r)return a;for(var s=0,l=i.length;s<l;s++)wo(i[s].query,e,n)&&o.push(s);return!o.length&&r&&(o=[-1]),o.length&&!Mo(o,this._currentMediaIndices)&&(a=Xy(o,function(t){return Uy(-1===t?r.option:i[t].option)})),this._currentMediaIndices=o,a}};var qy=d,$y=w,Ky=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"],Qy=function(t,e){qy(Po(t.series),function(t){$y(t)&&ko(t)});var n=["xAxis","yAxis","radiusAxis","angleAxis","singleAxis","parallelAxis","radar"];e&&n.push("valueAxis","categoryAxis","logAxis","timeAxis"),qy(n,function(e){qy(Po(t[e]),function(t){t&&(Do(t,"axisLabel"),Do(t.axisPointer,"label"))})}),qy(Po(t.parallel),function(t){var e=t&&t.parallelAxisDefault;Do(e,"axisLabel"),Do(e&&e.axisPointer,"label")}),qy(Po(t.calendar),function(t){Co(t,"itemStyle"),Do(t,"dayLabel"),Do(t,"monthLabel"),Do(t,"yearLabel")}),qy(Po(t.radar),function(t){Do(t,"name")}),qy(Po(t.geo),function(t){$y(t)&&(Ao(t),qy(Po(t.regions),function(t){Ao(t)}))}),qy(Po(t.timeline),function(t){Ao(t),Co(t,"label"),Co(t,"itemStyle"),Co(t,"controlStyle",!0);var e=t.data;y(e)&&d(e,function(t){w(t)&&(Co(t,"label"),Co(t,"itemStyle"))})}),qy(Po(t.toolbox),function(t){Co(t,"iconStyle"),qy(t.feature,function(t){Co(t,"iconStyle")})}),Do(Lo(t.axisPointer),"label"),Do(Lo(t.tooltip).axisPointer,"label")},Jy=[["x","left"],["y","top"],["x2","right"],["y2","bottom"]],tx=["grid","geo","parallel","legend","toolbox","title","visualMap","dataZoom","timeline"],ex=function(t,e){Qy(t,e),t.series=xn(t.series),d(t.series,function(t){if(w(t)){var e=t.type;if("pie"!==e&&"gauge"!==e||null!=t.clockWise&&(t.clockwise=t.clockWise),"gauge"===e){var n=Oo(t,"pointer.color");null!=n&&zo(t,"itemStyle.normal.color",n)}Eo(t)}}),t.dataRange&&(t.visualMap=t.dataRange),d(tx,function(e){var n=t[e];n&&(y(n)||(n=[n]),d(n,function(t){Eo(t)}))})},nx=Ro.prototype;nx.pure=!1,nx.persistent=!0,nx.getSource=function(){return this._source};var ix={arrayRows_column:{pure:!0,count:function(){return Math.max(0,this._data.length-this._source.startIndex)},getItem:function(t){return this._data[t+this._source.startIndex]},appendData:Fo},arrayRows_row:{pure:!0,count:function(){var t=this._data[0];return t?Math.max(0,t.length-this._source.startIndex):0},getItem:function(t){t+=this._source.startIndex;for(var e=[],n=this._data,i=0;i<n.length;i++){var r=n[i];e.push(r?r[t]:null)}return e},appendData:function(){throw new Error('Do not support appendData when set seriesLayoutBy: "row".')}},objectRows:{pure:!0,count:Bo,getItem:Vo,appendData:Fo},keyedColumns:{pure:!0,count:function(){var t=this._source.dimensionsDefine[0].name,e=this._data[t];return e?e.length:0},getItem:function(t){for(var e=[],n=this._source.dimensionsDefine,i=0;i<n.length;i++){var r=this._data[n[i].name];e.push(r?r[t]:null)}return e},appendData:function(t){var e=this._data;d(t,function(t,n){for(var i=e[n]||(e[n]=[]),r=0;r<(t||[]).length;r++)i.push(t[r])})}},original:{count:Bo,getItem:Vo,appendData:Fo},typedArray:{persistent:!1,pure:!0,count:function(){return this._data?this._data.length/this._dimSize:0},getItem:function(t,e){t-=this._offset,e=e||[];for(var n=this._dimSize*t,i=0;i<this._dimSize;i++)e[i]=this._data[n+i];return e},appendData:function(t){this._data=t},clean:function(){this._offset+=this.count(),this._data=null}}},rx={arrayRows:Ho,objectRows:function(t,e,n,i){return null!=n?t[i]:t},keyedColumns:Ho,original:function(t,e,n,i){var r=wn(t);return null!=n&&r instanceof Array?r[n]:r},typedArray:Ho},ox={arrayRows:Go,objectRows:function(t,e,n,i){return Wo(t[e],this._dimensionInfos[e])},keyedColumns:Go,original:function(t,e,n,i){var r=t&&(null==t.value?t:t.value);return!this._rawData.pure&&bn(t)&&(this.hasItemOption=!0),Wo(r instanceof Array?r[i]:r,this._dimensionInfos[e])},typedArray:function(t,e,n,i){return t[i]}},ax=/\{@(.+?)\}/g,sx={getDataParams:function(t,e){var n=this.getData(e),i=this.getRawValue(t,e),r=n.getRawIndex(t),o=n.getName(t),a=n.getRawDataItem(t),s=n.getItemVisual(t,"color");return{componentType:this.mainType,componentSubType:this.subType,seriesType:"series"===this.mainType?this.subType:null,seriesIndex:this.seriesIndex,seriesId:this.id,seriesName:this.name,name:o,dataIndex:r,data:a,dataType:e,value:i,color:s,marker:Rr(s),$vars:["seriesName","name","value"]}},getFormattedLabel:function(t,e,n,i,r){e=e||"normal";var o=this.getData(n),a=o.getItemModel(t),s=this.getDataParams(t,n);null!=i&&s.value instanceof Array&&(s.value=s.value[i]);var l=a.get("normal"===e?[r||"label","formatter"]:[e,r||"label","formatter"]);return"function"==typeof l?(s.status=e,l(s)):"string"==typeof l?Nr(l,s).replace(ax,function(e,n){var i=n.length;return"["===n.charAt(0)&&"]"===n.charAt(i-1)&&(n=+n.slice(1,i-1)),Zo(o,t,n)}):void 0},getRawValue:function(t,e){return Zo(this.getData(e),t)},formatTooltip:function(){}},lx=jo.prototype;lx.perform=function(t){function e(t){return!(t>=1)&&(t=1),t}var n=this._upstream,i=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this);var o;this._plan&&!i&&(o=this._plan(this.context));var a=e(this._modBy),s=this._modDataCount||0,l=e(t&&t.modBy),u=t&&t.modDataCount||0;a===l&&s===u||(o="reset");var h;(this._dirty||"reset"===o)&&(this._dirty=!1,h=qo(this,i)),this._modBy=l,this._modDataCount=u;var c=t&&t.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var d=this._dueIndex,f=Math.min(null!=c?this._dueIndex+c:1/0,this._dueEnd);if(!i&&(h||d<f)){var p=this._progress;if(y(p))for(var g=0;g<p.length;g++)Yo(this,p[g],d,f,l,u);else Yo(this,p,d,f,l,u)}this._dueIndex=f;var m=null!=this._settedOutputEnd?this._settedOutputEnd:f;this._outputDueEnd=m}else this._dueIndex=this._outputDueEnd=null!=this._settedOutputEnd?this._settedOutputEnd:this._dueEnd;return this.unfinished()};var ux=function(){function t(){return i<n?i++:null}function e(){var t=i%a*r+Math.ceil(i/a),e=i>=n?null:t<o?t:i;return i++,e}var n,i,r,o,a,s={reset:function(l,u,h,c){i=l,n=u,r=h,o=c,a=Math.ceil(o/r),s.next=r>1&&o>0?e:t}};return s}();lx.dirty=function(){this._dirty=!0,this._onDirty&&this._onDirty(this.context)},lx.unfinished=function(){return this._progress&&this._dueIndex<this._dueEnd},lx.pipe=function(t){(this._downstream!==t||this._dirty)&&(this._downstream=t,t._upstream=this,t.dirty())},lx.dispose=function(){this._disposed||(this._upstream&&(this._upstream._downstream=null),this._downstream&&(this._downstream._upstream=null),this._dirty=!1,this._disposed=!0)},lx.getUpstream=function(){return this._upstream},lx.getDownstream=function(){return this._downstream},lx.setOutputEnd=function(t){this._outputDueEnd=this._settedOutputEnd=t};var hx=Dn(),cx=Iy.extend({type:"series.__base__",seriesIndex:0,coordinateSystem:null,defaultOption:null,legendDataProvider:null,visualColorAccessPath:"itemStyle.color",layoutMode:null,init:function(t,e,n,i){this.seriesIndex=this.componentIndex,this.dataTask=Xo({count:Qo,reset:Jo}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n),to(this);var r=this.getInitialData(t,n);ea(r,this),this.dataTask.context.data=r,hx(this).dataBeforeProcessed=r,$o(this)},mergeDefaultAndTheme:function(t,e){var n=this.layoutMode,r=n?Ur(t):{},o=this.subType;Iy.hasClass(o)&&(o+="Series"),i(t,e.getTheme().get(this.subType)),i(t,this.getDefaultOption()),_n(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&Zr(t,r,n)},mergeOption:function(t,e){t=i(this.option,t,!0),this.fillDataTextStyle(t.data);var n=this.layoutMode;n&&Zr(this.option,t,n),to(this);var r=this.getInitialData(t,e);ea(r,this),this.dataTask.dirty(),this.dataTask.context.data=r,hx(this).dataBeforeProcessed=r,$o(this)},fillDataTextStyle:function(t){if(t&&!M(t))for(var e=["show"],n=0;n<t.length;n++)t[n]&&t[n].label&&_n(t[n],"label",e)},getInitialData:function(){},appendData:function(t){this.getRawData().appendData(t.data)},getData:function(t){var e=ia(this);if(e){var n=e.context.data;return null==t?n:n.getLinkedData(t)}return hx(this).data},setData:function(t){var e=ia(this);if(e){var n=e.context;n.data!==t&&e.modifyOutputEnd&&e.setOutputEnd(t.count()),n.outputData=t,e!==this.dataTask&&(n.data=t)}hx(this).data=t},getSource:function(){return Qr(this)},getRawData:function(){return hx(this).dataBeforeProcessed},getBaseAxis:function(){var t=this.coordinateSystem;return t&&t.getBaseAxis&&t.getBaseAxis()},formatTooltip:function(t,e,n){function i(t){return Er(Or(t))}var r=this.getData(),o=r.mapDimension("defaultedTooltip",!0),a=o.length,s=this.getRawValue(t),l=y(s),u=r.getItemVisual(t,"color");w(u)&&u.colorStops&&(u=(u.colorStops[0]||{}).color),u=u||"transparent";var h=a>1||l&&!a?function(n){function i(t,n){var i=r.getDimensionInfo(n);if(i&&!1!==i.otherDims.tooltip){var o=i.type,l=Rr({color:u,type:"subItem"}),h=(a?l+Er(i.displayName||"-")+": ":"")+Er("ordinal"===o?t+"":"time"===o?e?"":Vr("yyyy/MM/dd hh:mm:ss",t):Or(t));h&&s.push(h)}}var a=p(n,function(t,e,n){var i=r.getDimensionInfo(n);return t|=i&&!1!==i.tooltip&&null!=i.displayName},0),s=[];return o.length?d(o,function(e){i(Zo(r,t,e),e)}):d(n,i),(a?"<br/>":"")+s.join(a?"<br/>":", ")}(s):i(a?Zo(r,t,o[0]):l?s[0]:s),c=Rr(u),f=r.getName(t),g=this.name;return In(this)||(g=""),g=g?Er(g)+(e?": ":"<br/>"):"",e?c+g+h:g+c+(f?Er(f)+": "+h:h)},isAnimationEnabled:function(){if(bp.node)return!1;var t=this.getShallow("animation");return t&&this.getData().count()>this.getShallow("animationThreshold")&&(t=!1),t},restoreData:function(){this.dataTask.dirty()},getColorFromPalette:function(t,e,n){var i=this.ecModel,r=Ay.getColorFromPalette.call(this,t,e,n);return r||(r=i.getColorFromPalette(t,e,n)),r},coordDimToDataDim:function(t){return this.getRawData().mapDimension(t,!0)},getProgressive:function(){return this.get("progressive")},getProgressiveThreshold:function(){return this.get("progressiveThreshold")},getAxisTooltipData:null,getTooltipPosition:null,pipeTask:null,preventIncremental:null,pipelineContext:null});h(cx,sx),h(cx,Ay);var dx=function(){this.group=new Sg,this.uid=vr("viewComponent")};dx.prototype={constructor:dx,init:function(t,e){},render:function(t,e,n,i){},dispose:function(){}};var fx=dx.prototype;fx.updateView=fx.updateLayout=fx.updateVisual=function(t,e,n,i){},En(dx),Vn(dx,{registerWhenExtend:!0});var px=function(){var t=Dn();return function(e){var n=t(e),i=e.pipelineContext,r=n.large,o=n.progressiveRender,a=n.large=i.large,s=n.progressiveRender=i.progressiveRender;return!!(r^a||o^s)&&"reset"}},gx=Dn(),mx=px();ra.prototype={type:"chart",init:function(t,e){},render:function(t,e,n,i){},highlight:function(t,e,n,i){aa(t.getData(),i,"emphasis")},downplay:function(t,e,n,i){aa(t.getData(),i,"normal")},remove:function(t,e){this.group.removeAll()},dispose:function(){},incrementalPrepareRender:null,incrementalRender:null,updateTransform:null};var vx=ra.prototype;vx.updateView=vx.updateLayout=vx.updateVisual=function(t,e,n,i){this.render(t,e,n,i)},En(ra),Vn(ra,{registerWhenExtend:!0}),ra.markUpdateMethod=function(t,e){gx(t).updateMethod=e};var yx={incrementalPrepareRender:{progress:function(t,e){e.view.incrementalRender(t,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(t,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},xx="\0__throttleOriginMethod",_x="\0__throttleRate",bx="\0__throttleType",Mx={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData(),i=(t.visualColorAccessPath||"itemStyle.color").split("."),r=t.get(i)||t.getColorFromPalette(t.name,null,e.getSeriesCount());if(n.setVisual("color",r),!e.isSeriesFiltered(t)){"function"!=typeof r||r instanceof Xv||n.each(function(e){n.setItemVisual(e,"color",r(t.getDataParams(e)))});return{dataEach:n.hasItemOption?function(t,e){var n=t.getItemModel(e).get(i,!0);null!=n&&t.setItemVisual(e,"color",n)}:null}}}},Sx={toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}},Ix=function(t,e){function n(t,e){if("string"!=typeof t)return t;var n=t;return d(e,function(t,e){n=n.replace(new RegExp("\\{\\s*"+e+"\\s*\\}","g"),t)}),n}function i(t){var e=o.get(t);if(null==e){for(var n=t.split("."),i=Sx.aria,r=0;r<n.length;++r)i=i[n[r]];return i}return e}function r(t){return Sx.series.typeNames[t]||"自定义图"}var o=e.getModel("aria");if(o.get("show"))if(o.get("description"))t.setAttribute("aria-label",o.get("description"));else{var a=0;e.eachSeries(function(t,e){++a},this);var s,l=o.get("data.maxCount")||10,u=o.get("series.maxCount")||10,h=Math.min(a,u);if(!(a<1)){var c=function(){var t=e.getModel("title").option;return t&&t.length&&(t=t[0]),t&&t.text}();s=c?n(i("general.withTitle"),{title:c}):i("general.withoutTitle");var f=[];s+=n(i(a>1?"series.multiple.prefix":"series.single.prefix"),{seriesCount:a}),e.eachSeries(function(t,e){if(e<h){var o,s=t.get("name"),u="series."+(a>1?"multiple":"single")+".";o=n(o=i(s?u+"withName":u+"withoutName"),{seriesId:t.seriesIndex,seriesName:t.get("name"),seriesType:r(t.subType)});var c=t.getData();window.data=c,c.count()>l?o+=n(i("data.partialData"),{displayCnt:l}):o+=i("data.allData");for(var d=[],p=0;p<c.count();p++)if(p<l){var g=c.getName(p),m=Zo(c,p);d.push(n(i(g?"data.withName":"data.withoutName"),{name:g,value:m}))}o+=d.join(i("data.separator.middle"))+i("data.separator.end"),f.push(o)}}),s+=f.join(i("series.multiple.separator.middle"))+i("series.multiple.separator.end"),t.setAttribute("aria-label",s)}}},Cx=Math.PI,Tx=da.prototype;Tx.restoreData=function(t,e){t.restoreData(e),this._stageTaskMap.each(function(t){var e=t.overallTask;e&&e.dirty()})},Tx.getPerformArgs=function(t,e){if(t.__pipeline){var n=this._pipelineMap.get(t.__pipeline.id),i=n.context,r=!e&&n.progressiveEnabled&&(!i||i.progressiveRender)&&t.__idxInPipeline>n.blockIndex?n.step:null,o=i&&i.modDataCount;return{step:r,modBy:null!=o?Math.ceil(o/r):null,modDataCount:o}}},Tx.getPipeline=function(t){return this._pipelineMap.get(t)},Tx.updateStreamModes=function(t,e){var n=this._pipelineMap.get(t.uid),i=t.getData().count(),r=n.progressiveEnabled&&e.incrementalPrepareRender&&i>=n.threshold,o=t.get("large")&&i>=t.get("largeThreshold"),a="mod"===t.get("progressiveChunkMode")?i:null;t.pipelineContext=n.context={progressiveRender:r,modDataCount:a,large:o}},Tx.restorePipelines=function(t){var e=this,n=e._pipelineMap=N();t.eachSeries(function(t){var i=t.getProgressive(),r=t.uid;n.set(r,{id:r,head:null,tail:null,threshold:t.getProgressiveThreshold(),progressiveEnabled:i&&!(t.preventIncremental&&t.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),Sa(e,t,t.dataTask)})},Tx.prepareStageTasks=function(){var t=this._stageTaskMap,e=this.ecInstance.getModel(),n=this.api;d(this._allHandlers,function(i){var r=t.get(i.uid)||t.set(i.uid,[]);i.reset&&pa(this,i,r,e,n),i.overallReset&&ga(this,i,r,e,n)},this)},Tx.prepareView=function(t,e,n,i){var r=t.renderTask,o=r.context;o.model=e,o.ecModel=n,o.api=i,r.__block=!t.incrementalPrepareRender,Sa(this,e,r)},Tx.performDataProcessorTasks=function(t,e){fa(this,this._dataProcessorHandlers,t,e,{block:!0})},Tx.performVisualTasks=function(t,e,n){fa(this,this._visualHandlers,t,e,n)},Tx.performSeriesTasks=function(t){var e;t.eachSeries(function(t){e|=t.dataTask.perform()}),this.unfinished|=e},Tx.plan=function(){this._pipelineMap.each(function(t){var e=t.tail;do{if(e.__block){t.blockIndex=e.__idxInPipeline;break}e=e.getUpstream()}while(e)})};var Dx=Tx.updatePayload=function(t,e){"remain"!==e&&(t.context.payload=e)},Ax=ba(0);da.wrapStageHandler=function(t,e){return x(t)&&(t={overallReset:t,seriesType:Ia(t)}),t.uid=vr("stageHandler"),e&&(t.visualType=e),t};var kx,Px={},Lx={};Ca(Px,Hy),Ca(Lx,vo),Px.eachSeriesByType=Px.eachRawSeriesByType=function(t){kx=t},Px.eachComponent=function(t){"series"===t.mainType&&t.subType&&(kx=t.subType)};var Ox=["#37A2DA","#32C5E9","#67E0E3","#9FE6B8","#FFDB5C","#ff9f7f","#fb7293","#E062AE","#E690D1","#e7bcf3","#9d96f5","#8378EA","#96BFFF"],zx={color:Ox,colorLayer:[["#37A2DA","#ffd85c","#fd7b5f"],["#37A2DA","#67E0E3","#FFDB5C","#ff9f7f","#E062AE","#9d96f5"],["#37A2DA","#32C5E9","#9FE6B8","#FFDB5C","#ff9f7f","#fb7293","#e7bcf3","#8378EA","#96BFFF"],Ox]},Ex=["#dd6b66","#759aa0","#e69d87","#8dc1a9","#ea7e53","#eedd78","#73a373","#73b9bc","#7289ab","#91ca8c","#f49f42"],Nx={color:Ex,backgroundColor:"#333",tooltip:{axisPointer:{lineStyle:{color:"#eee"},crossStyle:{color:"#eee"}}},legend:{textStyle:{color:"#eee"}},textStyle:{color:"#eee"},title:{textStyle:{color:"#eee"}},toolbox:{iconStyle:{normal:{borderColor:"#eee"}}},dataZoom:{textStyle:{color:"#eee"}},visualMap:{textStyle:{color:"#eee"}},timeline:{lineStyle:{color:"#eee"},itemStyle:{normal:{color:Ex[1]}},label:{normal:{textStyle:{color:"#eee"}}},controlStyle:{normal:{color:"#eee",borderColor:"#eee"}}},timeAxis:{axisLine:{lineStyle:{color:"#eee"}},axisTick:{lineStyle:{color:"#eee"}},axisLabel:{textStyle:{color:"#eee"}},splitLine:{lineStyle:{type:"dashed",color:"#aaa"}},splitArea:{areaStyle:{color:"#eee"}}},logAxis:{axisLine:{lineStyle:{color:"#eee"}},axisTick:{lineStyle:{color:"#eee"}},axisLabel:{textStyle:{color:"#eee"}},splitLine:{lineStyle:{type:"dashed",color:"#aaa"}},splitArea:{areaStyle:{color:"#eee"}}},valueAxis:{axisLine:{lineStyle:{color:"#eee"}},axisTick:{lineStyle:{color:"#eee"}},axisLabel:{textStyle:{color:"#eee"}},splitLine:{lineStyle:{type:"dashed",color:"#aaa"}},splitArea:{areaStyle:{color:"#eee"}}},categoryAxis:{axisLine:{lineStyle:{color:"#eee"}},axisTick:{lineStyle:{color:"#eee"}},axisLabel:{textStyle:{color:"#eee"}},splitLine:{lineStyle:{type:"dashed",color:"#aaa"}},splitArea:{areaStyle:{color:"#eee"}}},line:{symbol:"circle"},graph:{color:Ex},gauge:{title:{textStyle:{color:"#eee"}}},candlestick:{itemStyle:{normal:{color:"#FD1050",color0:"#0CF49B",borderColor:"#FD1050",borderColor0:"#0CF49B"}}}};Nx.categoryAxis.splitLine.show=!1,Iy.extend({type:"dataset",defaultOption:{seriesLayoutBy:Ry,sourceHeader:null,dimensions:null,source:null},optionUpdated:function(){Kr(this)}}),dx.extend({type:"dataset"});var Rx=P,Bx=d,Vx=x,Fx=w,Hx=Iy.parseClassType,Gx={zrender:"4.0.4"},Wx=1e3,Zx=1e3,Ux=3e3,Xx={PROCESSOR:{FILTER:Wx,STATISTIC:5e3},VISUAL:{LAYOUT:Zx,GLOBAL:2e3,CHART:Ux,COMPONENT:4e3,BRUSH:5e3}},jx="__flagInMainProcess",Yx="__optionUpdated",qx=/^[a-zA-Z0-9_]+$/;Da.prototype.on=Ta("on"),Da.prototype.off=Ta("off"),Da.prototype.one=Ta("one"),h(Da,Zp);var $x=Aa.prototype;$x._onframe=function(){if(!this._disposed){var t=this._scheduler;if(this[Yx]){var e=this[Yx].silent;this[jx]=!0,Pa(this),Kx.update.call(this),this[jx]=!1,this[Yx]=!1,Ea.call(this,e),Na.call(this,e)}else if(t.unfinished){var n=1,i=this._model;this._api;t.unfinished=!1;do{var r=+new Date;t.performSeriesTasks(i),t.performDataProcessorTasks(i),Oa(this,i),t.performVisualTasks(i),Ga(this,this._model,0,"remain"),n-=+new Date-r}while(n>0&&t.unfinished);t.unfinished||this._zr.flush()}}},$x.getDom=function(){return this._dom},$x.getZr=function(){return this._zr},$x.setOption=function(t,e,n){var i;if(Fx(e)&&(n=e.lazyUpdate,i=e.silent,e=e.notMerge),this[jx]=!0,!this._model||e){var r=new xo(this._api),o=this._theme,a=this._model=new Hy(null,null,o,r);a.scheduler=this._scheduler,a.init(null,null,o,r)}this._model.setOption(t,n_),n?(this[Yx]={silent:i},this[jx]=!1):(Pa(this),Kx.update.call(this),this._zr.flush(),this[Yx]=!1,this[jx]=!1,Ea.call(this,i),Na.call(this,i))},$x.setTheme=function(){console.log("ECharts#setTheme() is DEPRECATED in ECharts 3.0")},$x.getModel=function(){return this._model},$x.getOption=function(){return this._model&&this._model.getOption()},$x.getWidth=function(){return this._zr.getWidth()},$x.getHeight=function(){return this._zr.getHeight()},$x.getDevicePixelRatio=function(){return this._zr.painter.dpr||window.devicePixelRatio||1},$x.getRenderedCanvas=function(t){if(bp.canvasSupported)return(t=t||{}).pixelRatio=t.pixelRatio||1,t.backgroundColor=t.backgroundColor||this._model.get("backgroundColor"),this._zr.painter.getRenderedCanvas(t)},$x.getSvgDataUrl=function(){if(bp.svgSupported){var t=this._zr;return d(t.storage.getDisplayList(),function(t){t.stopAnimation(!0)}),t.painter.pathToDataUrl()}},$x.getDataURL=function(t){var e=(t=t||{}).excludeComponents,n=this._model,i=[],r=this;Bx(e,function(t){n.eachComponent({mainType:t},function(t){var e=r._componentsMap[t.__viewId];e.group.ignore||(i.push(e),e.group.ignore=!0)})});var o="svg"===this._zr.painter.getType()?this.getSvgDataUrl():this.getRenderedCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return Bx(i,function(t){t.group.ignore=!1}),o},$x.getConnectedDataURL=function(t){if(bp.canvasSupported){var e=this.group,i=Math.min,r=Math.max;if(l_[e]){var o=1/0,a=1/0,s=-1/0,l=-1/0,u=[],h=t&&t.pixelRatio||1;d(s_,function(h,c){if(h.group===e){var d=h.getRenderedCanvas(n(t)),f=h.getDom().getBoundingClientRect();o=i(f.left,o),a=i(f.top,a),s=r(f.right,s),l=r(f.bottom,l),u.push({dom:d,left:f.left,top:f.top})}});var c=(s*=h)-(o*=h),f=(l*=h)-(a*=h),p=Op();p.width=c,p.height=f;var g=mn(p);return Bx(u,function(t){var e=new je({style:{x:t.left*h-o,y:t.top*h-a,image:t.dom}});g.add(e)}),g.refreshImmediately(),p.toDataURL("image/"+(t&&t.type||"png"))}return this.getDataURL(t)}},$x.convertToPixel=v(ka,"convertToPixel"),$x.convertFromPixel=v(ka,"convertFromPixel"),$x.containPixel=function(t,e){var n;return t=An(this._model,t),d(t,function(t,i){i.indexOf("Models")>=0&&d(t,function(t){var r=t.coordinateSystem;if(r&&r.containPoint)n|=!!r.containPoint(e);else if("seriesModels"===i){var o=this._chartsMap[t.__viewId];o&&o.containPoint&&(n|=o.containPoint(e,t))}},this)},this),!!n},$x.getVisual=function(t,e){var n=(t=An(this._model,t,{defaultMainType:"series"})).seriesModel.getData(),i=t.hasOwnProperty("dataIndexInside")?t.dataIndexInside:t.hasOwnProperty("dataIndex")?n.indexOfRawIndex(t.dataIndex):null;return null!=i?n.getItemVisual(i,e):n.getVisual(e)},$x.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},$x.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]};var Kx={prepareAndUpdate:function(t){Pa(this),Kx.update.call(this,t)},update:function(t){var e=this._model,n=this._api,i=this._zr,r=this._coordSysMgr,o=this._scheduler;if(e){o.restoreData(e,t),o.performSeriesTasks(e),r.create(e,n),o.performDataProcessorTasks(e,t),Oa(this,e),r.update(e,n),Va(e),o.performVisualTasks(e,t),Fa(this,e,n,t);var a=e.get("backgroundColor")||"transparent";if(bp.canvasSupported)i.setBackgroundColor(a);else{var s=St(a);a=Lt(s,"rgb"),0===s[3]&&(a="transparent")}Wa(e,n)}},updateTransform:function(t){var e=this._model,n=this,i=this._api;if(e){var r=[];e.eachComponent(function(o,a){var s=n.getViewOfComponentModel(a);if(s&&s.__alive)if(s.updateTransform){var l=s.updateTransform(a,e,i,t);l&&l.update&&r.push(s)}else r.push(s)});var o=N();e.eachSeries(function(r){var a=n._chartsMap[r.__viewId];if(a.updateTransform){var s=a.updateTransform(r,e,i,t);s&&s.update&&o.set(r.uid,1)}else o.set(r.uid,1)}),Va(e),this._scheduler.performVisualTasks(e,t,{setDirty:!0,dirtyMap:o}),Ga(n,e,0,t,o),Wa(e,this._api)}},updateView:function(t){var e=this._model;e&&(ra.markUpdateMethod(t,"updateView"),Va(e),this._scheduler.performVisualTasks(e,t,{setDirty:!0}),Fa(this,this._model,this._api,t),Wa(e,this._api))},updateVisual:function(t){Kx.update.call(this,t)},updateLayout:function(t){Kx.update.call(this,t)}};$x.resize=function(t){this._zr.resize(t);var e=this._model;if(this._loadingFX&&this._loadingFX.resize(),e){var n=e.resetOption("media"),i=t&&t.silent;this[jx]=!0,n&&Pa(this),Kx.update.call(this),this[jx]=!1,Ea.call(this,i),Na.call(this,i)}},$x.showLoading=function(t,e){if(Fx(t)&&(e=t,t=""),t=t||"default",this.hideLoading(),a_[t]){var n=a_[t](this._api,e),i=this._zr;this._loadingFX=n,i.add(n)}},$x.hideLoading=function(){this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null},$x.makeActionFromEvent=function(t){var e=o({},t);return e.type=t_[t.type],e},$x.dispatchAction=function(t,e){Fx(e)||(e={silent:!!e}),Jx[t.type]&&this._model&&(this[jx]?this._pendingActions.push(t):(za.call(this,t,e.silent),e.flush?this._zr.flush(!0):!1!==e.flush&&bp.browser.weChat&&this._throttledZrFlush(),Ea.call(this,e.silent),Na.call(this,e.silent)))},$x.appendData=function(t){var e=t.seriesIndex;this.getModel().getSeriesByIndex(e).appendData(t),this._scheduler.unfinished=!0},$x.on=Ta("on"),$x.off=Ta("off"),$x.one=Ta("one");var Qx=["click","dblclick","mouseover","mouseout","mousemove","mousedown","mouseup","globalout","contextmenu"];$x._initEvents=function(){Bx(Qx,function(t){this._zr.on(t,function(e){var n,i=this.getModel(),r=e.target;if("globalout"===t)n={};else if(r&&null!=r.dataIndex){var a=r.dataModel||i.getSeriesByIndex(r.seriesIndex);n=a&&a.getDataParams(r.dataIndex,r.dataType)||{}}else r&&r.eventData&&(n=o({},r.eventData));n&&(n.event=e,n.type=t,this.trigger(t,n))},this)},this),Bx(t_,function(t,e){this._messageCenter.on(e,function(t){this.trigger(e,t)},this)},this)},$x.isDisposed=function(){return this._disposed},$x.clear=function(){this.setOption({series:[]},!0)},$x.dispose=function(){if(!this._disposed){this._disposed=!0,Pn(this.getDom(),c_,"");var t=this._api,e=this._model;Bx(this._componentsViews,function(n){n.dispose(e,t)}),Bx(this._chartsViews,function(n){n.dispose(e,t)}),this._zr.dispose(),delete s_[this.id]}},h(Aa,Zp);var Jx={},t_={},e_=[],n_=[],i_=[],r_=[],o_={},a_={},s_={},l_={},u_=new Date-0,h_=new Date-0,c_="_echarts_instance_",d_={},f_=qa;ns(2e3,Mx),Qa(ex),Ja(5e3,function(t){var e=N();t.eachSeries(function(t){var n=t.get("stack");if(n){var i=e.get(n)||e.set(n,[]),r=t.getData(),o={stackResultDimension:r.getCalculationInfo("stackResultDimension"),stackedOverDimension:r.getCalculationInfo("stackedOverDimension"),stackedDimension:r.getCalculationInfo("stackedDimension"),stackedByDimension:r.getCalculationInfo("stackedByDimension"),isStackedByIndex:r.getCalculationInfo("isStackedByIndex"),data:r,seriesModel:t};if(!o.stackedDimension||!o.isStackedByIndex&&!o.stackedByDimension)return;i.length&&r.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(o)}}),e.each(No)}),rs("default",function(t,e){a(e=e||{},{text:"loading",color:"#c23531",textColor:"#000",maskColor:"rgba(255, 255, 255, 0.8)",zlevel:0});var n=new Fv({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4}),i=new Zv({shape:{startAngle:-Cx/2,endAngle:-Cx/2+.1,r:10},style:{stroke:e.color,lineCap:"round",lineWidth:5},zlevel:e.zlevel,z:10001}),r=new Fv({style:{fill:"none",text:e.text,textPosition:"right",textDistance:10,textFill:e.textColor},zlevel:e.zlevel,z:10001});i.animateShape(!0).when(1e3,{endAngle:3*Cx/2}).start("circularInOut"),i.animateShape(!0).when(1e3,{startAngle:3*Cx/2}).delay(300).start("circularInOut");var o=new Sg;return o.add(i),o.add(r),o.add(n),o.resize=function(){var e=t.getWidth()/2,o=t.getHeight()/2;i.setShape({cx:e,cy:o});var a=i.shape.r;r.setShape({x:e-a,y:o-a,width:2*a,height:2*a}),n.setShape({x:0,y:0,width:t.getWidth(),height:t.getHeight()})},o.resize(),o}),ts({type:"highlight",event:"highlight",update:"highlight"},R),ts({type:"downplay",event:"downplay",update:"downplay"},R),Ka("light",zx),Ka("dark",Nx);var p_={};hs.prototype={constructor:hs,add:function(t){return this._add=t,this},update:function(t){return this._update=t,this},remove:function(t){return this._remove=t,this},execute:function(){var t=this._old,e=this._new,n={},i=[],r=[];for(cs(t,{},i,"_oldKeyGetter",this),cs(e,n,r,"_newKeyGetter",this),o=0;o<t.length;o++)null!=(s=n[a=i[o]])?((u=s.length)?(1===u&&(n[a]=null),s=s.unshift()):n[a]=null,this._update&&this._update(s,o)):this._remove&&this._remove(o);for(var o=0;o<r.length;o++){var a=r[o];if(n.hasOwnProperty(a)){var s=n[a];if(null==s)continue;if(s.length)for(var l=0,u=s.length;l<u;l++)this._add&&this._add(s[l]);else this._add&&this._add(s)}}}};var g_=N(["tooltip","label","itemName","itemId","seriesName"]),m_=w,v_="e\0\0",y_={float:"undefined"==typeof Float64Array?Array:Float64Array,int:"undefined"==typeof Int32Array?Array:Int32Array,ordinal:Array,number:Array,time:Array},x_="undefined"==typeof Uint32Array?Array:Uint32Array,__="undefined"==typeof Uint16Array?Array:Uint16Array,w_=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_rawData","_chunkSize","_chunkCount","_dimValueGetter","_count","_rawCount","_nameDimIdx","_idDimIdx"],b_=["_extent","_approximateExtent","_rawExtent"],M_=function(t,e){t=t||["x","y"];for(var n={},i=[],r={},o=0;o<t.length;o++){var a=t[o];_(a)&&(a={name:a});var s=a.name;a.type=a.type||"float",a.coordDim||(a.coordDim=s,a.coordDimIndex=0),a.otherDims=a.otherDims||{},i.push(s),n[s]=a,a.index=o,a.createInvertedIndices&&(r[s]=[])}this.dimensions=i,this._dimensionInfos=n,this.hostModel=e,this.dataType,this._indices=null,this._count=0,this._rawCount=0,this._storage={},this._nameList=[],this._idList=[],this._optionModels=[],this._visual={},this._layout={},this._itemVisuals=[],this.hasItemVisual={},this._itemLayouts=[],this._graphicEls=[],this._chunkSize=1e5,this._chunkCount=0,this._rawData,this._rawExtent={},this._extent={},this._approximateExtent={},this._dimensionsSummary=ds(this),this._invertedIndicesMap=r,this._calculationInfo={}},S_=M_.prototype;S_.type="list",S_.hasItemOption=!0,S_.getDimension=function(t){return isNaN(t)||(t=this.dimensions[t]||t),t},S_.getDimensionInfo=function(t){return this._dimensionInfos[this.getDimension(t)]},S_.getDimensionsOnCoord=function(){return this._dimensionsSummary.dataDimsOnCoord.slice()},S_.mapDimension=function(t,e){var n=this._dimensionsSummary;if(null==e)return n.encodeFirstDimNotExtra[t];var i=n.encode[t];return!0===e?(i||[]).slice():i&&i[e]},S_.initData=function(t,e,n){($r.isInstance(t)||c(t))&&(t=new Ro(t,this.dimensions.length)),this._rawData=t,this._storage={},this._indices=null,this._nameList=e||[],this._idList=[],this._nameRepeatCount={},n||(this.hasItemOption=!1),this.defaultDimValueGetter=ox[this._rawData.getSource().sourceFormat],this._dimValueGetter=n=n||this.defaultDimValueGetter,this._rawExtent={},this._initDataFromProvider(0,t.count()),t.pure&&(this.hasItemOption=!1)},S_.getProvider=function(){return this._rawData},S_.appendData=function(t){var e=this._rawData,n=this.count();e.appendData(t);var i=e.count();e.persistent||(i+=n),this._initDataFromProvider(n,i)},S_._initDataFromProvider=function(t,e){if(!(t>=e)){for(var n,i=this._chunkSize,r=this._rawData,o=this._storage,a=this.dimensions,s=a.length,l=this._dimensionInfos,u=this._nameList,h=this._idList,c=this._rawExtent,d=this._nameRepeatCount={},f=this._chunkCount,p=f-1,g=0;g<s;g++){c[C=a[g]]||(c[C]=[1/0,-1/0]);var m=l[C];0===m.otherDims.itemName&&(n=this._nameDimIdx=g),0===m.otherDims.itemId&&(this._idDimIdx=g);var v=y_[m.type];o[C]||(o[C]=[]);var y=o[C][p];if(y&&y.length<i){for(var x=new v(Math.min(e-p*i,i)),_=0;_<y.length;_++)x[_]=y[_];o[C][p]=x}for(I=f*i;I<e;I+=i)o[C].push(new v(Math.min(e-I,i)));this._chunkCount=o[C].length}for(var w=new Array(s),b=t;b<e;b++){w=r.getItem(b,w);for(var M=Math.floor(b/i),S=b%i,I=0;I<s;I++){var C=a[I],T=o[C][M],D=this._dimValueGetter(w,C,b,I);T[S]=D;var A=c[C];D<A[0]&&(A[0]=D),D>A[1]&&(A[1]=D)}if(!r.pure){var k=u[b];if(w&&null==k)if(null!=w.name)u[b]=k=w.name;else if(null!=n){var P=a[n],L=o[P][M];if(L){k=L[S];var O=l[P].ordinalMeta;O&&O.categories.length&&(k=O.categories[k])}}var z=null==w?null:w.id;null==z&&null!=k&&(d[k]=d[k]||0,z=k,d[k]>0&&(z+="__ec__"+d[k]),d[k]++),null!=z&&(h[b]=z)}}!r.persistent&&r.clean&&r.clean(),this._rawCount=this._count=e,this._extent={},ys(this)}},S_.count=function(){return this._count},S_.getIndices=function(){var t=this._indices;if(t){var e=t.constructor,n=this._count;if(e===Array){i=new e(n);for(r=0;r<n;r++)i[r]=t[r]}else i=new e(t.buffer,0,n)}else for(var i=new(e=gs(this))(this.count()),r=0;r<i.length;r++)i[r]=r;return i},S_.get=function(t,e){if(!(e>=0&&e<this._count))return NaN;var n=this._storage;if(!n[t])return NaN;e=this.getRawIndex(e);var i=Math.floor(e/this._chunkSize),r=e%this._chunkSize;return n[t][i][r]},S_.getByRawIndex=function(t,e){if(!(e>=0&&e<this._rawCount))return NaN;var n=this._storage[t];if(!n)return NaN;var i=Math.floor(e/this._chunkSize),r=e%this._chunkSize;return n[i][r]},S_._getFast=function(t,e){var n=Math.floor(e/this._chunkSize),i=e%this._chunkSize;return this._storage[t][n][i]},S_.getValues=function(t,e){var n=[];y(t)||(e=t,t=this.dimensions);for(var i=0,r=t.length;i<r;i++)n.push(this.get(t[i],e));return n},S_.hasValue=function(t){for(var e=this._dimensionsSummary.dataDimsOnCoord,n=this._dimensionInfos,i=0,r=e.length;i<r;i++)if("ordinal"!==n[e[i]].type&&isNaN(this.get(e[i],t)))return!1;return!0},S_.getDataExtent=function(t){t=this.getDimension(t);var e=[1/0,-1/0];if(!this._storage[t])return e;var n,i=this.count();if(!this._indices)return this._rawExtent[t].slice();if(n=this._extent[t])return n.slice();for(var r=(n=e)[0],o=n[1],a=0;a<i;a++){var s=this._getFast(t,this.getRawIndex(a));s<r&&(r=s),s>o&&(o=s)}return n=[r,o],this._extent[t]=n,n},S_.getApproximateExtent=function(t){return t=this.getDimension(t),this._approximateExtent[t]||this.getDataExtent(t)},S_.setApproximateExtent=function(t,e){e=this.getDimension(e),this._approximateExtent[e]=t.slice()},S_.getCalculationInfo=function(t){return this._calculationInfo[t]},S_.setCalculationInfo=function(t,e){m_(t)?o(this._calculationInfo,t):this._calculationInfo[t]=e},S_.getSum=function(t){var e=0;if(this._storage[t])for(var n=0,i=this.count();n<i;n++){var r=this.get(t,n);isNaN(r)||(e+=r)}return e},S_.getMedian=function(t){var e=[];this.each(t,function(t,n){isNaN(t)||e.push(t)});var n=[].concat(e).sort(function(t,e){return t-e}),i=this.count();return 0===i?0:i%2==1?n[(i-1)/2]:(n[i/2]+n[i/2-1])/2},S_.rawIndexOf=function(t,e){var n=(t&&this._invertedIndicesMap[t])[e];return null==n||isNaN(n)?-1:n},S_.indexOfName=function(t){for(var e=0,n=this.count();e<n;e++)if(this.getName(e)===t)return e;return-1},S_.indexOfRawIndex=function(t){if(!this._indices)return t;if(t>=this._rawCount||t<0)return-1;var e=this._indices,n=e[t];if(null!=n&&n<this._count&&n===t)return t;for(var i=0,r=this._count-1;i<=r;){var o=(i+r)/2|0;if(e[o]<t)i=o+1;else{if(!(e[o]>t))return o;r=o-1}}return-1},S_.indicesOfNearest=function(t,e,n){var i=[];if(!this._storage[t])return i;null==n&&(n=1/0);for(var r=Number.MAX_VALUE,o=-1,a=0,s=this.count();a<s;a++){var l=e-this.get(t,a),u=Math.abs(l);l<=n&&u<=r&&((u<r||l>=0&&o<0)&&(r=u,o=l,i.length=0),i.push(a))}return i},S_.getRawIndex=_s,S_.getRawDataItem=function(t){if(this._rawData.persistent)return this._rawData.getItem(this.getRawIndex(t));for(var e=[],n=0;n<this.dimensions.length;n++){var i=this.dimensions[n];e.push(this.get(i,t))}return e},S_.getName=function(t){var e=this.getRawIndex(t);return this._nameList[e]||xs(this,this._nameDimIdx,e)||""},S_.getId=function(t){return bs(this,this.getRawIndex(t))},S_.each=function(t,e,n,i){if(this._count){"function"==typeof t&&(i=n,n=e,e=t,t=[]),n=n||i||this;for(var r=(t=f(Ms(t),this.getDimension,this)).length,o=0;o<this.count();o++)switch(r){case 0:e.call(n,o);break;case 1:e.call(n,this.get(t[0],o),o);break;case 2:e.call(n,this.get(t[0],o),this.get(t[1],o),o);break;default:for(var a=0,s=[];a<r;a++)s[a]=this.get(t[a],o);s[a]=o,e.apply(n,s)}}},S_.filterSelf=function(t,e,n,i){if(this._count){"function"==typeof t&&(i=n,n=e,e=t,t=[]),n=n||i||this,t=f(Ms(t),this.getDimension,this);for(var r=this.count(),o=new(gs(this))(r),a=[],s=t.length,l=0,u=t[0],h=0;h<r;h++){var c,d=this.getRawIndex(h);if(0===s)c=e.call(n,h);else if(1===s){var p=this._getFast(u,d);c=e.call(n,p,h)}else{for(var g=0;g<s;g++)a[g]=this._getFast(u,d);a[g]=h,c=e.apply(n,a)}c&&(o[l++]=d)}return l<r&&(this._indices=o),this._count=l,this._extent={},this.getRawIndex=this._indices?ws:_s,this}},S_.selectRange=function(t){if(this._count){var e=[];for(var n in t)t.hasOwnProperty(n)&&e.push(n);var i=e.length;if(i){var r=this.count(),o=new(gs(this))(r),a=0,s=e[0],l=t[s][0],u=t[s][1],h=!1;if(!this._indices){var c=0;if(1===i){for(var d=this._storage[e[0]],f=0;f<this._chunkCount;f++)for(var p=d[f],g=Math.min(this._count-f*this._chunkSize,this._chunkSize),m=0;m<g;m++)((w=p[m])>=l&&w<=u||isNaN(w))&&(o[a++]=c),c++;h=!0}else if(2===i){for(var d=this._storage[s],v=this._storage[e[1]],y=t[e[1]][0],x=t[e[1]][1],f=0;f<this._chunkCount;f++)for(var p=d[f],_=v[f],g=Math.min(this._count-f*this._chunkSize,this._chunkSize),m=0;m<g;m++){var w=p[m],b=_[m];(w>=l&&w<=u||isNaN(w))&&(b>=y&&b<=x||isNaN(b))&&(o[a++]=c),c++}h=!0}}if(!h)if(1===i)for(m=0;m<r;m++){S=this.getRawIndex(m);((w=this._getFast(s,S))>=l&&w<=u||isNaN(w))&&(o[a++]=S)}else for(m=0;m<r;m++){for(var M=!0,S=this.getRawIndex(m),f=0;f<i;f++){var I=e[f];((w=this._getFast(n,S))<t[I][0]||w>t[I][1])&&(M=!1)}M&&(o[a++]=this.getRawIndex(m))}return a<r&&(this._indices=o),this._count=a,this._extent={},this.getRawIndex=this._indices?ws:_s,this}}},S_.mapArray=function(t,e,n,i){"function"==typeof t&&(i=n,n=e,e=t,t=[]),n=n||i||this;var r=[];return this.each(t,function(){r.push(e&&e.apply(this,arguments))},n),r},S_.map=function(t,e,n,i){n=n||i||this;var r=Ss(this,t=f(Ms(t),this.getDimension,this));r._indices=this._indices,r.getRawIndex=r._indices?ws:_s;for(var o=r._storage,a=[],s=this._chunkSize,l=t.length,u=this.count(),h=[],c=r._rawExtent,d=0;d<u;d++){for(var p=0;p<l;p++)h[p]=this.get(t[p],d);h[l]=d;var g=e&&e.apply(n,h);if(null!=g){"object"!=typeof g&&(a[0]=g,g=a);for(var m=this.getRawIndex(d),v=Math.floor(m/s),y=m%s,x=0;x<g.length;x++){var _=t[x],w=g[x],b=c[_],M=o[_];M&&(M[v][y]=w),w<b[0]&&(b[0]=w),w>b[1]&&(b[1]=w)}}}return r},S_.downSample=function(t,e,n,i){for(var r=Ss(this,[t]),o=r._storage,a=[],s=Math.floor(1/e),l=o[t],u=this.count(),h=this._chunkSize,c=r._rawExtent[t],d=new(gs(this))(u),f=0,p=0;p<u;p+=s){s>u-p&&(s=u-p,a.length=s);for(var g=0;g<s;g++){var m=this.getRawIndex(p+g),v=Math.floor(m/h),y=m%h;a[g]=l[v][y]}var x=n(a),_=this.getRawIndex(Math.min(p+i(a,x)||0,u-1)),w=_%h;l[Math.floor(_/h)][w]=x,x<c[0]&&(c[0]=x),x>c[1]&&(c[1]=x),d[f++]=_}return r._count=f,r._indices=d,r.getRawIndex=ws,r},S_.getItemModel=function(t){var e=this.hostModel;return new pr(this.getRawDataItem(t),e,e&&e.ecModel)},S_.diff=function(t){var e=this;return new hs(t?t.getIndices():[],this.getIndices(),function(e){return bs(t,e)},function(t){return bs(e,t)})},S_.getVisual=function(t){var e=this._visual;return e&&e[t]},S_.setVisual=function(t,e){if(m_(t))for(var n in t)t.hasOwnProperty(n)&&this.setVisual(n,t[n]);else this._visual=this._visual||{},this._visual[t]=e},S_.setLayout=function(t,e){if(m_(t))for(var n in t)t.hasOwnProperty(n)&&this.setLayout(n,t[n]);else this._layout[t]=e},S_.getLayout=function(t){return this._layout[t]},S_.getItemLayout=function(t){return this._itemLayouts[t]},S_.setItemLayout=function(t,e,n){this._itemLayouts[t]=n?o(this._itemLayouts[t]||{},e):e},S_.clearItemLayouts=function(){this._itemLayouts.length=0},S_.getItemVisual=function(t,e,n){var i=this._itemVisuals[t],r=i&&i[e];return null!=r||n?r:this.getVisual(e)},S_.setItemVisual=function(t,e,n){var i=this._itemVisuals[t]||{},r=this.hasItemVisual;if(this._itemVisuals[t]=i,m_(e))for(var o in e)e.hasOwnProperty(o)&&(i[o]=e[o],r[o]=!0);else i[e]=n,r[e]=!0},S_.clearAllVisual=function(){this._visual={},this._itemVisuals=[],this.hasItemVisual={}};var I_=function(t){t.seriesIndex=this.seriesIndex,t.dataIndex=this.dataIndex,t.dataType=this.dataType};S_.setItemGraphicEl=function(t,e){var n=this.hostModel;e&&(e.dataIndex=t,e.dataType=this.dataType,e.seriesIndex=n&&n.seriesIndex,"group"===e.type&&e.traverse(I_,e)),this._graphicEls[t]=e},S_.getItemGraphicEl=function(t){return this._graphicEls[t]},S_.eachItemGraphicEl=function(t,e){d(this._graphicEls,function(n,i){n&&t&&t.call(e,n,i)})},S_.cloneShallow=function(t){if(!t){var e=f(this.dimensions,this.getDimensionInfo,this);t=new M_(e,this.hostModel)}if(t._storage=this._storage,vs(t,this),this._indices){var n=this._indices.constructor;t._indices=new n(this._indices)}else t._indices=null;return t.getRawIndex=t._indices?ws:_s,t},S_.wrapMethod=function(t,e){var n=this[t];"function"==typeof n&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var t=n.apply(this,arguments);return e.apply(this,[t].concat(A(arguments)))})},S_.TRANSFERABLE_METHODS=["cloneShallow","downSample","map"],S_.CHANGABLE_METHODS=["filterSelf","selectRange"];var C_=function(t,e){return e=e||{},Ts(e.coordDimensions||[],t,{dimsDef:e.dimensionsDefine||t.dimensionsDefine,encodeDef:e.encodeDefine||t.encodeDefine,dimCount:e.dimensionsCount,generateCoord:e.generateCoord,generateCoordCount:e.generateCoordCount})};Ns.prototype.parse=function(t){return t},Ns.prototype.getSetting=function(t){return this._setting[t]},Ns.prototype.contain=function(t){var e=this._extent;return t>=e[0]&&t<=e[1]},Ns.prototype.normalize=function(t){var e=this._extent;return e[1]===e[0]?.5:(t-e[0])/(e[1]-e[0])},Ns.prototype.scale=function(t){var e=this._extent;return t*(e[1]-e[0])+e[0]},Ns.prototype.unionExtent=function(t){var e=this._extent;t[0]<e[0]&&(e[0]=t[0]),t[1]>e[1]&&(e[1]=t[1])},Ns.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},Ns.prototype.getExtent=function(){return this._extent.slice()},Ns.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=t),isNaN(e)||(n[1]=e)},Ns.prototype.isBlank=function(){return this._isBlank},Ns.prototype.setBlank=function(t){this._isBlank=t},Ns.prototype.getLabel=null,En(Ns),Vn(Ns,{registerWhenExtend:!0}),Rs.createByAxisModel=function(t){var e=t.option,n=e.data,i=n&&f(n,Vs);return new Rs({categories:i,needCollect:!i,deduplication:!1!==e.dedplication})};var T_=Rs.prototype;T_.getOrdinal=function(t){return Bs(this).get(t)},T_.parseAndCollect=function(t){var e,n=this._needCollect;if("string"!=typeof t&&!n)return t;if(n&&!this._deduplication)return e=this.categories.length,this.categories[e]=t,e;var i=Bs(this);return null==(e=i.get(t))&&(n?(e=this.categories.length,this.categories[e]=t,i.set(t,e)):e=NaN),e};var D_=Ns.prototype,A_=Ns.extend({type:"ordinal",init:function(t,e){t&&!y(t)||(t=new Rs({categories:t})),this._ordinalMeta=t,this._extent=e||[0,t.categories.length-1]},parse:function(t){return"string"==typeof t?this._ordinalMeta.getOrdinal(t):Math.round(t)},contain:function(t){return t=this.parse(t),D_.contain.call(this,t)&&null!=this._ordinalMeta.categories[t]},normalize:function(t){return D_.normalize.call(this,this.parse(t))},scale:function(t){return Math.round(D_.scale.call(this,t))},getTicks:function(){for(var t=[],e=this._extent,n=e[0];n<=e[1];)t.push(n),n++;return t},getLabel:function(t){if(!this.isBlank())return this._ordinalMeta.categories[t]},count:function(){return this._extent[1]-this._extent[0]+1},unionExtentFromData:function(t,e){this.unionExtent(t.getApproximateExtent(e))},getOrdinalMeta:function(){return this._ordinalMeta},niceTicks:R,niceExtent:R});A_.create=function(){return new A_};var k_=wr,P_=wr,L_=Ns.extend({type:"interval",_interval:0,_intervalPrecision:2,setExtent:function(t,e){var n=this._extent;isNaN(t)||(n[0]=parseFloat(t)),isNaN(e)||(n[1]=parseFloat(e))},unionExtent:function(t){var e=this._extent;t[0]<e[0]&&(e[0]=t[0]),t[1]>e[1]&&(e[1]=t[1]),L_.prototype.setExtent.call(this,e[0],e[1])},getInterval:function(){return this._interval},setInterval:function(t){this._interval=t,this._niceExtent=this._extent.slice(),this._intervalPrecision=Hs(t)},getTicks:function(){return Zs(this._interval,this._extent,this._niceExtent,this._intervalPrecision)},getLabel:function(t,e){if(null==t)return"";var n=e&&e.precision;return null==n?n=Sr(t)||0:"auto"===n&&(n=this._intervalPrecision),t=P_(t,n,!0),Or(t)},niceTicks:function(t,e,n){t=t||5;var i=this._extent,r=i[1]-i[0];if(isFinite(r)){r<0&&(r=-r,i.reverse());var o=Fs(i,t,e,n);this._intervalPrecision=o.intervalPrecision,this._interval=o.interval,this._niceExtent=o.niceTickExtent}},niceExtent:function(t){var e=this._extent;if(e[0]===e[1])if(0!==e[0]){var n=e[0];t.fixMax?e[0]-=n/2:(e[1]+=n/2,e[0]-=n/2)}else e[1]=1;var i=e[1]-e[0];isFinite(i)||(e[0]=0,e[1]=1),this.niceTicks(t.splitNumber,t.minInterval,t.maxInterval);var r=this._interval;t.fixMin||(e[0]=P_(Math.floor(e[0]/r)*r)),t.fixMax||(e[1]=P_(Math.ceil(e[1]/r)*r))}});L_.create=function(){return new L_};var O_="__ec_stack_",z_="undefined"!=typeof Float32Array?Float32Array:Array,E_={seriesType:"bar",plan:px(),reset:function(t){if(Ks(t)&&Qs(t)){var e=t.getData(),n=t.coordinateSystem,i=n.getBaseAxis(),r=n.getOtherAxis(i),o=e.mapDimension(r.dim),a=e.mapDimension(i.dim),s=r.isHorizontal(),l=s?0:1,u=$s(Ys([t]),i,t).width;return u>.5||(u=.5),{progress:function(t,e){for(var h,c=new z_(2*t.count),d=[],f=[],p=0;null!=(h=t.next());)f[l]=e.get(o,h),f[1-l]=e.get(a,h),d=n.dataToPoint(f,null,d),c[p++]=d[0],c[p++]=d[1];e.setLayout({largePoints:c,barWidth:u,valueAxisStart:Js(i,r,!1),valueAxisHorizontal:s})}}}}},N_=L_.prototype,R_=Math.ceil,B_=Math.floor,V_=function(t,e,n,i){for(;n<i;){var r=n+i>>>1;t[r][1]<e?n=r+1:i=r}return n},F_=L_.extend({type:"time",getLabel:function(t){var e=this._stepLvl,n=new Date(t);return Vr(e[0],n,this.getSetting("useUTC"))},niceExtent:function(t){var e=this._extent;if(e[0]===e[1]&&(e[0]-=864e5,e[1]+=864e5),e[1]===-1/0&&e[0]===1/0){var n=new Date;e[1]=+new Date(n.getFullYear(),n.getMonth(),n.getDate()),e[0]=e[1]-864e5}this.niceTicks(t.splitNumber,t.minInterval,t.maxInterval);var i=this._interval;t.fixMin||(e[0]=wr(B_(e[0]/i)*i)),t.fixMax||(e[1]=wr(R_(e[1]/i)*i))},niceTicks:function(t,e,n){t=t||10;var i=this._extent,r=i[1]-i[0],o=r/t;null!=e&&o<e&&(o=e),null!=n&&o>n&&(o=n);var a=H_.length,s=V_(H_,o,0,a),l=H_[Math.min(s,a-1)],u=l[1];"year"===l[0]&&(u*=Lr(r/u/t,!0));var h=this.getSetting("useUTC")?0:60*new Date(+i[0]||+i[1]).getTimezoneOffset()*1e3,c=[Math.round(R_((i[0]-h)/u)*u+h),Math.round(B_((i[1]-h)/u)*u+h)];Ws(c,i),this._stepLvl=l,this._interval=u,this._niceExtent=c},parse:function(t){return+Ar(t)}});d(["contain","normalize"],function(t){F_.prototype[t]=function(e){return N_[t].call(this,this.parse(e))}});var H_=[["hh:mm:ss",1e3],["hh:mm:ss",5e3],["hh:mm:ss",1e4],["hh:mm:ss",15e3],["hh:mm:ss",3e4],["hh:mm\nMM-dd",6e4],["hh:mm\nMM-dd",3e5],["hh:mm\nMM-dd",6e5],["hh:mm\nMM-dd",9e5],["hh:mm\nMM-dd",18e5],["hh:mm\nMM-dd",36e5],["hh:mm\nMM-dd",72e5],["hh:mm\nMM-dd",216e5],["hh:mm\nMM-dd",432e5],["MM-dd\nyyyy",864e5],["MM-dd\nyyyy",1728e5],["MM-dd\nyyyy",2592e5],["MM-dd\nyyyy",3456e5],["MM-dd\nyyyy",432e6],["MM-dd\nyyyy",5184e5],["week",6048e5],["MM-dd\nyyyy",864e6],["week",12096e5],["week",18144e5],["month",26784e5],["week",36288e5],["month",53568e5],["week",36288e5],["quarter",8208e6],["month",107136e5],["month",13392e6],["half-year",16416e6],["month",214272e5],["month",26784e6],["year",32832e6]];F_.create=function(t){return new F_({useUTC:t.ecModel.get("useUTC")})};var G_=Ns.prototype,W_=L_.prototype,Z_=Sr,U_=wr,X_=Math.floor,j_=Math.ceil,Y_=Math.pow,q_=Math.log,$_=Ns.extend({type:"log",base:10,$constructor:function(){Ns.apply(this,arguments),this._originalScale=new L_},getTicks:function(){var t=this._originalScale,e=this._extent,n=t.getExtent();return f(W_.getTicks.call(this),function(i){var r=wr(Y_(this.base,i));return r=i===e[0]&&t.__fixMin?tl(r,n[0]):r,r=i===e[1]&&t.__fixMax?tl(r,n[1]):r},this)},getLabel:W_.getLabel,scale:function(t){return t=G_.scale.call(this,t),Y_(this.base,t)},setExtent:function(t,e){var n=this.base;t=q_(t)/q_(n),e=q_(e)/q_(n),W_.setExtent.call(this,t,e)},getExtent:function(){var t=this.base,e=G_.getExtent.call(this);e[0]=Y_(t,e[0]),e[1]=Y_(t,e[1]);var n=this._originalScale,i=n.getExtent();return n.__fixMin&&(e[0]=tl(e[0],i[0])),n.__fixMax&&(e[1]=tl(e[1],i[1])),e},unionExtent:function(t){this._originalScale.unionExtent(t);var e=this.base;t[0]=q_(t[0])/q_(e),t[1]=q_(t[1])/q_(e),G_.unionExtent.call(this,t)},unionExtentFromData:function(t,e){this.unionExtent(t.getApproximateExtent(e))},niceTicks:function(t){t=t||10;var e=this._extent,n=e[1]-e[0];if(!(n===1/0||n<=0)){var i=kr(n);for(t/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var r=[wr(j_(e[0]/i)*i),wr(X_(e[1]/i)*i)];this._interval=i,this._niceExtent=r}},niceExtent:function(t){W_.niceExtent.call(this,t);var e=this._originalScale;e.__fixMin=t.fixMin,e.__fixMax=t.fixMax}});d(["contain","normalize"],function(t){$_.prototype[t]=function(e){return e=q_(e)/q_(this.base),G_[t].call(this,e)}}),$_.create=function(){return new $_};var K_={getMin:function(t){var e=this.option,n=t||null==e.rangeStart?e.min:e.rangeStart;return this.axis&&null!=n&&"dataMin"!==n&&"function"!=typeof n&&!I(n)&&(n=this.axis.scale.parse(n)),n},getMax:function(t){var e=this.option,n=t||null==e.rangeEnd?e.max:e.rangeEnd;return this.axis&&null!=n&&"dataMax"!==n&&"function"!=typeof n&&!I(n)&&(n=this.axis.scale.parse(n)),n},getNeedCrossZero:function(){var t=this.option;return null==t.rangeStart&&null==t.rangeEnd&&!t.scale},getCoordSysModel:R,setRange:function(t,e){this.option.rangeStart=t,this.option.rangeEnd=e},resetRange:function(){this.option.rangeStart=this.option.rangeEnd=null}},Q_=Ai({type:"triangle",shape:{cx:0,cy:0,width:0,height:0},buildPath:function(t,e){var n=e.cx,i=e.cy,r=e.width/2,o=e.height/2;t.moveTo(n,i-o),t.lineTo(n+r,i+o),t.lineTo(n-r,i+o),t.closePath()}}),J_=Ai({type:"diamond",shape:{cx:0,cy:0,width:0,height:0},buildPath:function(t,e){var n=e.cx,i=e.cy,r=e.width/2,o=e.height/2;t.moveTo(n,i-o),t.lineTo(n+r,i),t.lineTo(n,i+o),t.lineTo(n-r,i),t.closePath()}}),tw=Ai({type:"pin",shape:{x:0,y:0,width:0,height:0},buildPath:function(t,e){var n=e.x,i=e.y,r=e.width/5*3,o=Math.max(r,e.height),a=r/2,s=a*a/(o-a),l=i-o+a+s,u=Math.asin(s/a),h=Math.cos(u)*a,c=Math.sin(u),d=Math.cos(u),f=.6*a,p=.7*a;t.moveTo(n-h,l+s),t.arc(n,l,a,Math.PI-u,2*Math.PI+u),t.bezierCurveTo(n+h-c*f,l+s+d*f,n,i-p,n,i),t.bezierCurveTo(n,i-p,n-h+c*f,l+s+d*f,n-h,l+s),t.closePath()}}),ew=Ai({type:"arrow",shape:{x:0,y:0,width:0,height:0},buildPath:function(t,e){var n=e.height,i=e.width,r=e.x,o=e.y,a=i/3*2;t.moveTo(r,o),t.lineTo(r+a,o+n),t.lineTo(r,o+n/4*3),t.lineTo(r-a,o+n),t.lineTo(r,o),t.closePath()}}),nw={line:function(t,e,n,i,r){r.x1=t,r.y1=e+i/2,r.x2=t+n,r.y2=e+i/2},rect:function(t,e,n,i,r){r.x=t,r.y=e,r.width=n,r.height=i},roundRect:function(t,e,n,i,r){r.x=t,r.y=e,r.width=n,r.height=i,r.r=Math.min(n,i)/4},square:function(t,e,n,i,r){var o=Math.min(n,i);r.x=t,r.y=e,r.width=o,r.height=o},circle:function(t,e,n,i,r){r.cx=t+n/2,r.cy=e+i/2,r.r=Math.min(n,i)/2},diamond:function(t,e,n,i,r){r.cx=t+n/2,r.cy=e+i/2,r.width=n,r.height=i},pin:function(t,e,n,i,r){r.x=t+n/2,r.y=e+i/2,r.width=n,r.height=i},arrow:function(t,e,n,i,r){r.x=t+n/2,r.y=e+i/2,r.width=n,r.height=i},triangle:function(t,e,n,i,r){r.cx=t+n/2,r.cy=e+i/2,r.width=n,r.height=i}},iw={};d({line:Hv,rect:Fv,roundRect:Fv,square:Fv,circle:Pv,diamond:J_,pin:tw,arrow:ew,triangle:Q_},function(t,e){iw[e]=new t});var rw=Ai({type:"symbol",shape:{symbolType:"",x:0,y:0,width:0,height:0},beforeBrush:function(){var t=this.style;"pin"===this.shape.symbolType&&"inside"===t.textPosition&&(t.textPosition=["50%","40%"],t.textAlign="center",t.textVerticalAlign="middle")},buildPath:function(t,e,n){var i=e.symbolType,r=iw[i];"none"!==e.symbolType&&(r||(r=iw[i="rect"]),nw[i](e.x,e.y,e.width,e.height,r.shape),r.buildPath(t,r.shape,n))}}),ow={isDimensionStacked:Ps,enableDataStack:ks,getStackedDimension:Ls},aw=(Object.freeze||Object)({createList:function(t){return Os(t.getSource(),t)},getLayoutRect:Gr,dataStack:ow,createScale:function(t,e){var n=e;pr.isInstance(e)||h(n=new pr(e),K_);var i=rl(n);return i.setExtent(t[0],t[1]),il(i,n),i},mixinAxisModelCommonMethods:function(t){h(t,K_)},completeDimensions:Ts,createDimensions:C_,createSymbol:cl}),sw=1e-8;pl.prototype={constructor:pl,properties:null,getBoundingRect:function(){var t=this._rect;if(t)return t;for(var e=Number.MAX_VALUE,n=[e,e],i=[-e,-e],r=[],o=[],a=this.geometries,s=0;s<a.length;s++)"polygon"===a[s].type&&(ti(a[s].exterior,r,o),K(n,n,r),Q(i,i,o));return 0===s&&(n[0]=n[1]=i[0]=i[1]=0),this._rect=new Xt(n[0],n[1],i[0]-n[0],i[1]-n[1])},contain:function(t){var e=this.getBoundingRect(),n=this.geometries;if(!e.contain(t[0],t[1]))return!1;t:for(var i=0,r=n.length;i<r;i++)if("polygon"===n[i].type){var o=n[i].exterior,a=n[i].interiors;if(fl(o,t[0],t[1])){for(var s=0;s<(a?a.length:0);s++)if(fl(a[s]))continue t;return!0}}return!1},transformTo:function(t,e,n,i){var r=this.getBoundingRect(),o=r.width/r.height;n?i||(i=n/o):n=o*i;for(var a=new Xt(t,e,n,i),s=r.calculateTransform(a),l=this.geometries,u=0;u<l.length;u++)if("polygon"===l[u].type){for(var h=l[u].exterior,c=l[u].interiors,d=0;d<h.length;d++)$(h[d],h[d],s);for(var f=0;f<(c?c.length:0);f++)for(d=0;d<c[f].length;d++)$(c[f][d],c[f][d],s)}(r=this._rect).copy(a),this.center=[r.x+r.width/2,r.y+r.height/2]}};var lw=function(t){return gl(t),f(g(t.features,function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0}),function(t){var e=t.properties,n=t.geometry,i=n.coordinates,r=[];"Polygon"===n.type&&r.push({type:"polygon",exterior:i[0],interiors:i.slice(1)}),"MultiPolygon"===n.type&&d(i,function(t){t[0]&&r.push({type:"polygon",exterior:t[0],interiors:t.slice(1)})});var o=new pl(e.name,r,e.cp);return o.properties=e,o})},uw=Dn(),hw=[0,1],cw=function(t,e,n){this.dim=t,this.scale=e,this._extent=n||[0,0],this.inverse=!1,this.onBand=!1};cw.prototype={constructor:cw,contain:function(t){var e=this._extent,n=Math.min(e[0],e[1]),i=Math.max(e[0],e[1]);return t>=n&&t<=i},containData:function(t){return this.contain(this.dataToCoord(t))},getExtent:function(){return this._extent.slice()},getPixelPrecision:function(t){return Ir(t||this.scale.getExtent(),this._extent)},setExtent:function(t,e){var n=this._extent;n[0]=t,n[1]=e},dataToCoord:function(t,e){var n=this._extent,i=this.scale;return t=i.normalize(t),this.onBand&&"ordinal"===i.type&&Ll(n=n.slice(),i.count()),xr(t,hw,n,e)},coordToData:function(t,e){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&Ll(n=n.slice(),i.count());var r=xr(t,n,hw,e);return this.scale.scale(r)},pointToData:function(t,e){},getTicksCoords:function(t){var e=(t=t||{}).tickModel||this.getTickModel(),n=yl(this,e),i=f(n.ticks,function(t){return{coord:this.dataToCoord(t),tickValue:t}},this),r=e.get("alignWithLabel");return Ol(this,i,n.tickCategoryInterval,r,t.clamp),i},getViewLabels:function(){return vl(this).labels},getLabelModel:function(){return this.model.getModel("axisLabel")},getTickModel:function(){return this.model.getModel("axisTick")},getBandWidth:function(){var t=this._extent,e=this.scale.getExtent(),n=e[1]-e[0]+(this.onBand?1:0);0===n&&(n=1);var i=Math.abs(t[1]-t[0]);return Math.abs(i)/n},isHorizontal:null,getRotate:null,calculateCategoryInterval:function(){return Tl(this)}};var dw=lw,fw={};d(["map","each","filter","indexOf","inherits","reduce","filter","bind","curry","isArray","isString","isObject","isFunction","extend","defaults","clone","merge"],function(t){fw[t]=Np[t]}),cx.extend({type:"series.line",dependencies:["grid","polar"],getInitialData:function(t,e){return Os(this.getSource(),this)},defaultOption:{zlevel:0,z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,hoverAnimation:!0,clipOverflow:!0,label:{position:"top"},lineStyle:{width:2,type:"solid"},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0}});var pw=El.prototype,gw=El.getSymbolSize=function(t,e){var n=t.getItemVisual(e,"symbolSize");return n instanceof Array?n.slice():[+n,+n]};pw._createSymbol=function(t,e,n,i,r){this.removeAll();var o=cl(t,-1,-1,2,2,e.getItemVisual(n,"color"),r);o.attr({z2:100,culling:!0,scale:Nl(i)}),o.drift=Rl,this._symbolType=t,this.add(o)},pw.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(t)},pw.getSymbolPath=function(){return this.childAt(0)},pw.getScale=function(){return this.childAt(0).scale},pw.highlight=function(){this.childAt(0).trigger("emphasis")},pw.downplay=function(){this.childAt(0).trigger("normal")},pw.setZ=function(t,e){var n=this.childAt(0);n.zlevel=t,n.z=e},pw.setDraggable=function(t){var e=this.childAt(0);e.draggable=t,e.cursor=t?"move":"pointer"},pw.updateData=function(t,e,n){this.silent=!1;var i=t.getItemVisual(e,"symbol")||"circle",r=t.hostModel,o=gw(t,e),a=i!==this._symbolType;if(a){var s=t.getItemVisual(e,"symbolKeepAspect");this._createSymbol(i,t,e,o,s)}else(l=this.childAt(0)).silent=!1,ar(l,{scale:Nl(o)},r,e);if(this._updateCommon(t,e,o,n),a){var l=this.childAt(0),u=n&&n.fadeIn,h={scale:l.scale.slice()};u&&(h.style={opacity:l.style.opacity}),l.scale=[0,0],u&&(l.style.opacity=0),sr(l,h,r,e)}this._seriesModel=r};var mw=["itemStyle"],vw=["emphasis","itemStyle"],yw=["label"],xw=["emphasis","label"];pw._updateCommon=function(t,e,n,i){var r=this.childAt(0),a=t.hostModel,s=t.getItemVisual(e,"color");"image"!==r.type&&r.useStyle({strokeNoScale:!0});var l=i&&i.itemStyle,u=i&&i.hoverItemStyle,h=i&&i.symbolRotate,c=i&&i.symbolOffset,d=i&&i.labelModel,f=i&&i.hoverLabelModel,p=i&&i.hoverAnimation,g=i&&i.cursorStyle;if(!i||t.hasItemOption){var m=i&&i.itemModel?i.itemModel:t.getItemModel(e);l=m.getModel(mw).getItemStyle(["color"]),u=m.getModel(vw).getItemStyle(),h=m.getShallow("symbolRotate"),c=m.getShallow("symbolOffset"),d=m.getModel(yw),f=m.getModel(xw),p=m.getShallow("hoverAnimation"),g=m.getShallow("cursor")}else u=o({},u);var v=r.style;r.attr("rotation",(h||0)*Math.PI/180||0),c&&r.attr("position",[_r(c[0],n[0]),_r(c[1],n[1])]),g&&r.attr("cursor",g),r.setColor(s,i&&i.symbolInnerColor),r.setStyle(l);var y=t.getItemVisual(e,"opacity");null!=y&&(v.opacity=y);var x=t.getItemVisual(e,"liftZ"),_=r.__z2Origin;null!=x?null==_&&(r.__z2Origin=r.z2,r.z2+=x):null!=_&&(r.z2=_,r.__z2Origin=null);var w=i&&i.useNameLabel;$i(v,u,d,f,{labelFetcher:a,labelDataIndex:e,defaultText:function(e,n){return w?t.getName(e):zl(t,e)},isRectText:!0,autoColor:s}),r.off("mouseover").off("mouseout").off("emphasis").off("normal"),r.hoverStyle=u,qi(r);var b=Nl(n);if(p&&a.isAnimationEnabled()){var M=function(){if(!this.incremental){var t=b[1]/b[0];this.animateTo({scale:[Math.max(1.1*b[0],b[0]+3),Math.max(1.1*b[1],b[1]+3*t)]},400,"elasticOut")}},S=function(){this.incremental||this.animateTo({scale:b},400,"elasticOut")};r.on("mouseover",M).on("mouseout",S).on("emphasis",M).on("normal",S)}},pw.fadeOut=function(t,e){var n=this.childAt(0);this.silent=n.silent=!0,!(e&&e.keepLabel)&&(n.style.text=null),ar(n,{style:{opacity:0},scale:[0,0]},this._seriesModel,this.dataIndex,t)},u(El,Sg);var _w=Bl.prototype;_w.updateData=function(t,e){e=Fl(e);var n=this.group,i=t.hostModel,r=this._data,o=this._symbolCtor,a=Hl(t);r||n.removeAll(),t.diff(r).add(function(i){var r=t.getItemLayout(i);if(Vl(t,r,i,e)){var s=new o(t,i,a);s.attr("position",r),t.setItemGraphicEl(i,s),n.add(s)}}).update(function(s,l){var u=r.getItemGraphicEl(l),h=t.getItemLayout(s);Vl(t,h,s,e)?(u?(u.updateData(t,s,a),ar(u,{position:h},i)):(u=new o(t,s)).attr("position",h),n.add(u),t.setItemGraphicEl(s,u)):n.remove(u)}).remove(function(t){var e=r.getItemGraphicEl(t);e&&e.fadeOut(function(){n.remove(e)})}).execute(),this._data=t},_w.isPersistent=function(){return!0},_w.updateLayout=function(){var t=this._data;t&&t.eachItemGraphicEl(function(e,n){var i=t.getItemLayout(n);e.attr("position",i)})},_w.incrementalPrepareUpdate=function(t){this._seriesScope=Hl(t),this._data=null,this.group.removeAll()},_w.incrementalUpdate=function(t,e,n){n=Fl(n);for(var i=t.start;i<t.end;i++){var r=e.getItemLayout(i);if(Vl(e,r,i,n)){var o=new this._symbolCtor(e,i,this._seriesScope);o.traverse(function(t){t.isGroup||(t.incremental=t.useHoverLayer=!0)}),o.attr("position",r),this.group.add(o),e.setItemGraphicEl(i,o)}}},_w.remove=function(t){var e=this.group,n=this._data;n&&t?n.eachItemGraphicEl(function(t){t.fadeOut(function(){e.remove(t)})}):e.removeAll()};var ww=function(t,e,n,i,r,o,a,s){for(var l=Ul(t,e),u=[],h=[],c=[],d=[],f=[],p=[],g=[],m=Gl(r,e,a),v=Gl(o,t,s),y=0;y<l.length;y++){var x=l[y],_=!0;switch(x.cmd){case"=":var w=t.getItemLayout(x.idx),b=e.getItemLayout(x.idx1);(isNaN(w[0])||isNaN(w[1]))&&(w=b.slice()),u.push(w),h.push(b),c.push(n[x.idx]),d.push(i[x.idx1]),g.push(e.getRawIndex(x.idx1));break;case"+":M=x.idx;u.push(r.dataToPoint([e.get(m.dataDimsForPoint[0],M),e.get(m.dataDimsForPoint[1],M)])),h.push(e.getItemLayout(M).slice()),c.push(Zl(m,r,e,M)),d.push(i[M]),g.push(e.getRawIndex(M));break;case"-":var M=x.idx,S=t.getRawIndex(M);S!==M?(u.push(t.getItemLayout(M)),h.push(o.dataToPoint([t.get(v.dataDimsForPoint[0],M),t.get(v.dataDimsForPoint[1],M)])),c.push(n[M]),d.push(Zl(v,o,t,M)),g.push(S)):_=!1}_&&(f.push(x),p.push(p.length))}p.sort(function(t,e){return g[t]-g[e]});for(var I=[],C=[],T=[],D=[],A=[],y=0;y<p.length;y++){M=p[y];I[y]=u[M],C[y]=h[M],T[y]=c[M],D[y]=d[M],A[y]=f[M]}return{current:I,next:C,stackedOnCurrent:T,stackedOnNext:D,status:A}},bw=K,Mw=Q,Sw=G,Iw=V,Cw=[],Tw=[],Dw=[],Aw=xi.extend({type:"ec-polyline",shape:{points:[],smooth:0,smoothConstraint:!0,smoothMonotone:null,connectNulls:!1},style:{fill:null,stroke:"#000"},brush:Ov(xi.prototype.brush),buildPath:function(t,e){var n=e.points,i=0,r=n.length,o=$l(n,e.smoothConstraint);if(e.connectNulls){for(;r>0&&Xl(n[r-1]);r--);for(;i<r&&Xl(n[i]);i++);}for(;i<r;)i+=jl(t,n,i,r,r,1,o.min,o.max,e.smooth,e.smoothMonotone,e.connectNulls)+1}}),kw=xi.extend({type:"ec-polygon",shape:{points:[],stackedOnPoints:[],smooth:0,stackedOnSmooth:0,smoothConstraint:!0,smoothMonotone:null,connectNulls:!1},brush:Ov(xi.prototype.brush),buildPath:function(t,e){var n=e.points,i=e.stackedOnPoints,r=0,o=n.length,a=e.smoothMonotone,s=$l(n,e.smoothConstraint),l=$l(i,e.smoothConstraint);if(e.connectNulls){for(;o>0&&Xl(n[o-1]);o--);for(;r<o&&Xl(n[r]);r++);}for(;r<o;){var u=jl(t,n,r,o,o,1,s.min,s.max,e.smooth,a,e.connectNulls);jl(t,i,r+u-1,u,o,-1,l.min,l.max,e.stackedOnSmooth,a,e.connectNulls),r+=u+1,t.closePath()}}});ra.extend({type:"line",init:function(){var t=new Sg,e=new Bl;this.group.add(e.group),this._symbolDraw=e,this._lineGroup=t},render:function(t,e,n){var i=t.coordinateSystem,r=this.group,o=t.getData(),s=t.getModel("lineStyle"),l=t.getModel("areaStyle"),u=o.mapArray(o.getItemLayout),h="polar"===i.type,c=this._coordSys,d=this._symbolDraw,f=this._polyline,p=this._polygon,g=this._lineGroup,m=t.get("animation"),v=!l.isEmpty(),y=l.get("origin"),x=tu(i,o,Gl(i,o,y)),_=t.get("showSymbol"),w=_&&!h&&au(t,o,i),b=this._data;b&&b.eachItemGraphicEl(function(t,e){t.__temp&&(r.remove(t),b.setItemGraphicEl(e,null))}),_||d.remove(),r.add(g);var M=!h&&t.get("step");f&&c.type===i.type&&M===this._step?(v&&!p?p=this._newPolygon(u,x,i,m):p&&!v&&(g.remove(p),p=this._polygon=null),g.setClipPath(iu(i,!1,!1,t)),_&&d.updateData(o,{isIgnore:w,clipShape:iu(i,!1,!0,t)}),o.eachItemGraphicEl(function(t){t.stopAnimation(!0)}),Kl(this._stackedOnPoints,x)&&Kl(this._points,u)||(m?this._updateAnimation(o,x,i,n,M,y):(M&&(u=ru(u,i,M),x=ru(x,i,M)),f.setShape({points:u}),p&&p.setShape({points:u,stackedOnPoints:x})))):(_&&d.updateData(o,{isIgnore:w,clipShape:iu(i,!1,!0,t)}),M&&(u=ru(u,i,M),x=ru(x,i,M)),f=this._newPolyline(u,i,m),v&&(p=this._newPolygon(u,x,i,m)),g.setClipPath(iu(i,!0,!1,t)));var S=ou(o,i)||o.getVisual("color");f.useStyle(a(s.getLineStyle(),{fill:"none",stroke:S,lineJoin:"bevel"}));var I=t.get("smooth");if(I=Ql(t.get("smooth")),f.setShape({smooth:I,smoothMonotone:t.get("smoothMonotone"),connectNulls:t.get("connectNulls")}),p){var C=o.getCalculationInfo("stackedOnSeries"),T=0;p.useStyle(a(l.getAreaStyle(),{fill:S,opacity:.7,lineJoin:"bevel"})),C&&(T=Ql(C.get("smooth"))),p.setShape({smooth:I,stackedOnSmooth:T,smoothMonotone:t.get("smoothMonotone"),connectNulls:t.get("connectNulls")})}this._data=o,this._coordSys=i,this._stackedOnPoints=x,this._points=u,this._step=M,this._valueOrigin=y},dispose:function(){},highlight:function(t,e,n,i){var r=t.getData(),o=Tn(r,i);if(!(o instanceof Array)&&null!=o&&o>=0){var a=r.getItemGraphicEl(o);if(!a){var s=r.getItemLayout(o);if(!s)return;(a=new El(r,o)).position=s,a.setZ(t.get("zlevel"),t.get("z")),a.ignore=isNaN(s[0])||isNaN(s[1]),a.__temp=!0,r.setItemGraphicEl(o,a),a.stopSymbolAnimation(!0),this.group.add(a)}a.highlight()}else ra.prototype.highlight.call(this,t,e,n,i)},downplay:function(t,e,n,i){var r=t.getData(),o=Tn(r,i);if(null!=o&&o>=0){var a=r.getItemGraphicEl(o);a&&(a.__temp?(r.setItemGraphicEl(o,null),this.group.remove(a)):a.downplay())}else ra.prototype.downplay.call(this,t,e,n,i)},_newPolyline:function(t){var e=this._polyline;return e&&this._lineGroup.remove(e),e=new Aw({shape:{points:t},silent:!0,z2:10}),this._lineGroup.add(e),this._polyline=e,e},_newPolygon:function(t,e){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new kw({shape:{points:t,stackedOnPoints:e},silent:!0}),this._lineGroup.add(n),this._polygon=n,n},_updateAnimation:function(t,e,n,i,r,o){var a=this._polyline,s=this._polygon,l=t.hostModel,u=ww(this._data,t,this._stackedOnPoints,e,this._coordSys,n,this._valueOrigin,o),h=u.current,c=u.stackedOnCurrent,d=u.next,f=u.stackedOnNext;r&&(h=ru(u.current,n,r),c=ru(u.stackedOnCurrent,n,r),d=ru(u.next,n,r),f=ru(u.stackedOnNext,n,r)),a.shape.__points=u.current,a.shape.points=h,ar(a,{shape:{points:d}},l),s&&(s.setShape({points:h,stackedOnPoints:c}),ar(s,{shape:{points:d,stackedOnPoints:f}},l));for(var p=[],g=u.status,m=0;m<g.length;m++)if("="===g[m].cmd){var v=t.getItemGraphicEl(g[m].idx1);v&&p.push({el:v,ptIdx:m})}a.animators&&a.animators.length&&a.animators[0].during(function(){for(var t=0;t<p.length;t++)p[t].el.attr("position",a.shape.__points[p[t].ptIdx])})},remove:function(t){var e=this.group,n=this._data;this._lineGroup.removeAll(),this._symbolDraw.remove(!0),n&&n.eachItemGraphicEl(function(t,i){t.__temp&&(e.remove(t),n.setItemGraphicEl(i,null))}),this._polyline=this._polygon=this._coordSys=this._points=this._stackedOnPoints=this._data=null}});var Pw=function(t,e,n){return{seriesType:t,performRawSeries:!0,reset:function(t,i,r){var o=t.getData(),a=t.get("symbol")||e,s=t.get("symbolSize"),l=t.get("symbolKeepAspect");if(o.setVisual({legendSymbol:n||a,symbol:a,symbolSize:s,symbolKeepAspect:l}),!i.isSeriesFiltered(t)){var u="function"==typeof s;return{dataEach:o.hasItemOption||u?function(e,n){if("function"==typeof s){var i=t.getRawValue(n),r=t.getDataParams(n);e.setItemVisual(n,"symbolSize",s(i,r))}if(e.hasItemOption){var o=e.getItemModel(n),a=o.getShallow("symbol",!0),l=o.getShallow("symbolSize",!0),u=o.getShallow("symbolKeepAspect",!0);null!=a&&e.setItemVisual(n,"symbol",a),null!=l&&e.setItemVisual(n,"symbolSize",l),null!=u&&e.setItemVisual(n,"symbolKeepAspect",u)}}:null}}}}},Lw=function(t){return{seriesType:t,plan:px(),reset:function(t){var e=t.getData(),n=t.coordinateSystem,i=t.pipelineContext.large;if(n){var r=f(n.dimensions,function(t){return e.mapDimension(t)}).slice(0,2),o=r.length,a=e.getCalculationInfo("stackResultDimension");return Ps(e,r[0])&&(r[0]=a),Ps(e,r[1])&&(r[1]=a),o&&{progress:function(t,e){for(var a=t.end-t.start,s=i&&new Float32Array(a*o),l=t.start,u=0,h=[],c=[];l<t.end;l++){var d;if(1===o)f=e.get(r[0],l),d=!isNaN(f)&&n.dataToPoint(f,null,c);else{var f=h[0]=e.get(r[0],l),p=h[1]=e.get(r[1],l);d=!isNaN(f)&&!isNaN(p)&&n.dataToPoint(h,null,c)}i?(s[u++]=d?d[0]:NaN,s[u++]=d?d[1]:NaN):e.setItemLayout(l,d&&d.slice()||[NaN,NaN])}i&&e.setLayout("symbolPoints",s)}}}}}},Ow={average:function(t){for(var e=0,n=0,i=0;i<t.length;i++)isNaN(t[i])||(e+=t[i],n++);return 0===n?NaN:e/n},sum:function(t){for(var e=0,n=0;n<t.length;n++)e+=t[n]||0;return e},max:function(t){for(var e=-1/0,n=0;n<t.length;n++)t[n]>e&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;n<t.length;n++)t[n]<e&&(e=t[n]);return isFinite(e)?e:NaN},nearest:function(t){return t[0]}},zw=function(t,e){return Math.round(t.length/2)},Ew=function(t){this._axes={},this._dimList=[],this.name=t||""};Ew.prototype={constructor:Ew,type:"cartesian",getAxis:function(t){return this._axes[t]},getAxes:function(){return f(this._dimList,lu,this)},getAxesByScale:function(t){return t=t.toLowerCase(),g(this.getAxes(),function(e){return e.scale.type===t})},addAxis:function(t){var e=t.dim;this._axes[e]=t,this._dimList.push(e)},dataToCoord:function(t){return this._dataCoordConvert(t,"dataToCoord")},coordToData:function(t){return this._dataCoordConvert(t,"coordToData")},_dataCoordConvert:function(t,e){for(var n=this._dimList,i=t instanceof Array?[]:{},r=0;r<n.length;r++){var o=n[r],a=this._axes[o];i[o]=a[e](t[o])}return i}},uu.prototype={constructor:uu,type:"cartesian2d",dimensions:["x","y"],getBaseAxis:function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},containPoint:function(t){var e=this.getAxis("x"),n=this.getAxis("y");return e.contain(e.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},containData:function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},dataToPoint:function(t,e,n){var i=this.getAxis("x"),r=this.getAxis("y");return n=n||[],n[0]=i.toGlobalCoord(i.dataToCoord(t[0])),n[1]=r.toGlobalCoord(r.dataToCoord(t[1])),n},clampData:function(t,e){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,r=n.getExtent(),o=i.getExtent(),a=n.parse(t[0]),s=i.parse(t[1]);return e=e||[],e[0]=Math.min(Math.max(Math.min(r[0],r[1]),a),Math.max(r[0],r[1])),e[1]=Math.min(Math.max(Math.min(o[0],o[1]),s),Math.max(o[0],o[1])),e},pointToData:function(t,e){var n=this.getAxis("x"),i=this.getAxis("y");return e=e||[],e[0]=n.coordToData(n.toLocalCoord(t[0])),e[1]=i.coordToData(i.toLocalCoord(t[1])),e},getOtherAxis:function(t){return this.getAxis("x"===t.dim?"y":"x")}},u(uu,Ew);var Nw=function(t,e,n,i,r){cw.call(this,t,e,n),this.type=i||"value",this.position=r||"bottom"};Nw.prototype={constructor:Nw,index:0,getAxesOnZeroOf:null,model:null,isHorizontal:function(){var t=this.position;return"top"===t||"bottom"===t},getGlobalExtent:function(t){var e=this.getExtent();return e[0]=this.toGlobalCoord(e[0]),e[1]=this.toGlobalCoord(e[1]),t&&e[0]>e[1]&&e.reverse(),e},getOtherAxis:function(){this.grid.getOtherAxis()},pointToData:function(t,e){return this.coordToData(this.toLocalCoord(t["x"===this.dim?0:1]),e)},toLocalCoord:null,toGlobalCoord:null},u(Nw,cw);var Rw={show:!0,zlevel:0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#333",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#ccc"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.3)","rgba(200,200,200,0.3)"]}}},Bw={};Bw.categoryAxis=i({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},Rw),Bw.valueAxis=i({boundaryGap:[0,0],splitNumber:5},Rw),Bw.timeAxis=a({scale:!0,min:"dataMin",max:"dataMax"},Bw.valueAxis),Bw.logAxis=a({scale:!0,logBase:10},Bw.valueAxis);var Vw=["value","category","time","log"],Fw=function(t,e,n,o){d(Vw,function(a){e.extend({type:t+"Axis."+a,mergeDefaultAndTheme:function(e,r){var o=this.layoutMode,s=o?Ur(e):{};i(e,r.getTheme().get(a+"Axis")),i(e,this.getDefaultOption()),e.type=n(t,e),o&&Zr(e,s,o)},optionUpdated:function(){"category"===this.option.type&&(this.__ordinalMeta=Rs.createByAxisModel(this))},getCategories:function(t){var e=this.option;if("category"===e.type)return t?e.data:this.__ordinalMeta.categories},getOrdinalMeta:function(){return this.__ordinalMeta},defaultOption:r([{},Bw[a+"Axis"],o],!0)})}),Iy.registerSubTypeDefaulter(t+"Axis",v(n,t))},Hw=Iy.extend({type:"cartesian2dAxis",axis:null,init:function(){Hw.superApply(this,"init",arguments),this.resetRange()},mergeOption:function(){Hw.superApply(this,"mergeOption",arguments),this.resetRange()},restoreData:function(){Hw.superApply(this,"restoreData",arguments),this.resetRange()},getCoordSysModel:function(){return this.ecModel.queryComponents({mainType:"grid",index:this.option.gridIndex,id:this.option.gridId})[0]}});i(Hw.prototype,K_);var Gw={offset:0};Fw("x",Hw,hu,Gw),Fw("y",Hw,hu,Gw),Iy.extend({type:"grid",dependencies:["xAxis","yAxis"],layoutMode:"box",coordinateSystem:null,defaultOption:{show:!1,zlevel:0,z:0,left:"10%",top:60,right:"10%",bottom:60,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"}});var Ww=du.prototype;Ww.type="grid",Ww.axisPointerEnabled=!0,Ww.getRect=function(){return this._rect},Ww.update=function(t,e){var n=this._axesMap;this._updateScale(t,this.model),d(n.x,function(t){il(t.scale,t.model)}),d(n.y,function(t){il(t.scale,t.model)}),d(n.x,function(t){fu(n,"y",t)}),d(n.y,function(t){fu(n,"x",t)}),this.resize(this.model,e)},Ww.resize=function(t,e,n){function i(){d(o,function(t){var e=t.isHorizontal(),n=e?[0,r.width]:[0,r.height],i=t.inverse?1:0;t.setExtent(n[i],n[1-i]),gu(t,e?r.x:r.y)})}var r=Gr(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()});this._rect=r;var o=this._axesList;i(),!n&&t.get("containLabel")&&(d(o,function(t){if(!t.model.get("axisLabel.inside")){var e=ll(t);if(e){var n=t.isHorizontal()?"height":"width",i=t.model.get("axisLabel.margin");r[n]-=e[n]+i,"top"===t.position?r.y+=e.height+i:"left"===t.position&&(r.x+=e.width+i)}}}),i())},Ww.getAxis=function(t,e){var n=this._axesMap[t];if(null!=n){if(null==e)for(var i in n)if(n.hasOwnProperty(i))return n[i];return n[e]}},Ww.getAxes=function(){return this._axesList.slice()},Ww.getCartesian=function(t,e){if(null!=t&&null!=e){var n="x"+t+"y"+e;return this._coordsMap[n]}w(t)&&(e=t.yAxisIndex,t=t.xAxisIndex);for(var i=0,r=this._coordsList;i<r.length;i++)if(r[i].getAxis("x").index===t||r[i].getAxis("y").index===e)return r[i]},Ww.getCartesians=function(){return this._coordsList.slice()},Ww.convertToPixel=function(t,e,n){var i=this._findConvertTarget(t,e);return i.cartesian?i.cartesian.dataToPoint(n):i.axis?i.axis.toGlobalCoord(i.axis.dataToCoord(n)):null},Ww.convertFromPixel=function(t,e,n){var i=this._findConvertTarget(t,e);return i.cartesian?i.cartesian.pointToData(n):i.axis?i.axis.coordToData(i.axis.toLocalCoord(n)):null},Ww._findConvertTarget=function(t,e){var n,i,r=e.seriesModel,o=e.xAxisModel||r&&r.getReferringComponents("xAxis")[0],a=e.yAxisModel||r&&r.getReferringComponents("yAxis")[0],s=e.gridModel,u=this._coordsList;return r?l(u,n=r.coordinateSystem)<0&&(n=null):o&&a?n=this.getCartesian(o.componentIndex,a.componentIndex):o?i=this.getAxis("x",o.componentIndex):a?i=this.getAxis("y",a.componentIndex):s&&s.coordinateSystem===this&&(n=this._coordsList[0]),{cartesian:n,axis:i}},Ww.containPoint=function(t){var e=this._coordsList[0];if(e)return e.containPoint(t)},Ww._initCartesian=function(t,e,n){function i(n){return function(i,s){if(cu(i,t,e)){var l=i.get("position");"x"===n?"top"!==l&&"bottom"!==l&&r[l="bottom"]&&(l="top"===l?"bottom":"top"):"left"!==l&&"right"!==l&&r[l="left"]&&(l="left"===l?"right":"left"),r[l]=!0;var u=new Nw(n,rl(i),[0,0],i.get("type"),l),h="category"===u.type;u.onBand=h&&i.get("boundaryGap"),u.inverse=i.get("inverse"),i.axis=u,u.model=i,u.grid=this,u.index=s,this._axesList.push(u),o[n][s]=u,a[n]++}}}var r={left:!1,right:!1,top:!1,bottom:!1},o={x:{},y:{}},a={x:0,y:0};if(e.eachComponent("xAxis",i("x"),this),e.eachComponent("yAxis",i("y"),this),!a.x||!a.y)return this._axesMap={},void(this._axesList=[]);this._axesMap=o,d(o.x,function(e,n){d(o.y,function(i,r){var o="x"+n+"y"+r,a=new uu(o);a.grid=this,a.model=t,this._coordsMap[o]=a,this._coordsList.push(a),a.addAxis(e),a.addAxis(i)},this)},this)},Ww._updateScale=function(t,e){function n(t,e,n){d(t.mapDimension(e.dim,!0),function(n){e.scale.unionExtentFromData(t,Ls(t,n))})}d(this._axesList,function(t){t.scale.setExtent(1/0,-1/0)}),t.eachSeries(function(i){if(vu(i)){var r=mu(i),o=r[0],a=r[1];if(!cu(o,e,t)||!cu(a,e,t))return;var s=this.getCartesian(o.componentIndex,a.componentIndex),l=i.getData(),u=s.getAxis("x"),h=s.getAxis("y");"list"===l.type&&(n(l,u),n(l,h))}},this)},Ww.getTooltipAxes=function(t){var e=[],n=[];return d(this.getCartesians(),function(i){var r=null!=t&&"auto"!==t?i.getAxis(t):i.getBaseAxis(),o=i.getOtherAxis(r);l(e,r)<0&&e.push(r),l(n,o)<0&&n.push(o)}),{baseAxes:e,otherAxes:n}};var Zw=["xAxis","yAxis"];du.create=function(t,e){var n=[];return t.eachComponent("grid",function(i,r){var o=new du(i,t,e);o.name="grid_"+r,o.resize(i,e,!0),i.coordinateSystem=o,n.push(o)}),t.eachSeries(function(t){if(vu(t)){var e=mu(t),n=e[0],i=e[1],r=n.getCoordSysModel().coordinateSystem;t.coordinateSystem=r.getCartesian(n.componentIndex,i.componentIndex)}}),n},du.dimensions=du.prototype.dimensions=uu.prototype.dimensions,yo.register("cartesian2d",du);var Uw=Math.PI,Xw=function(t,e){this.opt=e,this.axisModel=t,a(e,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0}),this.group=new Sg;var n=new Sg({position:e.position.slice(),rotation:e.rotation});n.updateTransform(),this._transform=n.transform,this._dumbGroup=n};Xw.prototype={constructor:Xw,hasBuilder:function(t){return!!jw[t]},add:function(t){jw[t].call(this)},getGroup:function(){return this.group}};var jw={axisLine:function(){var t=this.opt,e=this.axisModel;if(e.get("axisLine.show")){var n=this.axisModel.axis.getExtent(),i=this._transform,r=[n[0],0],a=[n[1],0];i&&($(r,r,i),$(a,a,i));var s=o({lineCap:"round"},e.getModel("axisLine.lineStyle").getLineStyle());this.group.add(new Hv(zi({anid:"line",shape:{x1:r[0],y1:r[1],x2:a[0],y2:a[1]},style:s,strokeContainThreshold:t.strokeContainThreshold||5,silent:!0,z2:1})));var l=e.get("axisLine.symbol"),u=e.get("axisLine.symbolSize"),h=e.get("axisLine.symbolOffset")||0;if("number"==typeof h&&(h=[h,h]),null!=l){"string"==typeof l&&(l=[l,l]),"string"!=typeof u&&"number"!=typeof u||(u=[u,u]);var c=u[0],f=u[1];d([{rotate:t.rotation+Math.PI/2,offset:h[0],r:0},{rotate:t.rotation-Math.PI/2,offset:h[1],r:Math.sqrt((r[0]-a[0])*(r[0]-a[0])+(r[1]-a[1])*(r[1]-a[1]))}],function(e,n){if("none"!==l[n]&&null!=l[n]){var i=cl(l[n],-c/2,-f/2,c,f,s.stroke,!0),o=e.r+e.offset,a=[r[0]+o*Math.cos(t.rotation),r[1]-o*Math.sin(t.rotation)];i.attr({rotation:e.rotate,position:a,silent:!0}),this.group.add(i)}},this)}}},axisTickLabel:function(){var t=this.axisModel,e=this.opt,n=Iu(this,t,e);wu(t,Cu(this,t,e),n)},axisName:function(){var t=this.opt,e=this.axisModel,n=C(t.axisName,e.get("name"));if(n){var i,r=e.get("nameLocation"),a=t.nameDirection,s=e.getModel("nameTextStyle"),l=e.get("nameGap")||0,u=this.axisModel.axis.getExtent(),h=u[0]>u[1]?-1:1,c=["start"===r?u[0]-h*l:"end"===r?u[1]+h*l:(u[0]+u[1])/2,Su(r)?t.labelOffset+a*l:0],d=e.get("nameRotate");null!=d&&(d=d*Uw/180);var f;Su(r)?i=Yw(t.rotation,null!=d?d:t.rotation,a):(i=xu(t,r,d||0,u),null!=(f=t.axisNameAvailableWidth)&&(f=Math.abs(f/Math.sin(i.rotation)),!isFinite(f)&&(f=null)));var p=s.getFont(),g=e.get("nameTruncate",!0)||{},m=g.ellipsis,v=C(t.nameTruncateMaxWidth,g.maxWidth,f),y=null!=m&&null!=v?my(n,v,p,m,{minChar:2,placeholder:g.placeholder}):n,x=e.get("tooltip",!0),_=e.mainType,w={componentType:_,name:n,$vars:["name"]};w[_+"Index"]=e.componentIndex;var b=new kv({anid:"name",__fullText:n,__truncatedText:y,position:c,rotation:i.rotation,silent:_u(e),z2:1,tooltip:x&&x.show?o({content:n,formatter:function(){return n},formatterParams:w},x):null});Ki(b.style,s,{text:y,textFont:p,textFill:s.getTextColor()||e.get("axisLine.lineStyle.color"),textAlign:i.textAlign,textVerticalAlign:i.textVerticalAlign}),e.get("triggerEvent")&&(b.eventData=yu(e),b.eventData.targetType="axisName",b.eventData.name=n),this._dumbGroup.add(b),b.updateTransform(),this.group.add(b),b.decomposeTransform()}}},Yw=Xw.innerTextLayout=function(t,e,n){var i,r,o=Tr(e-t);return Dr(o)?(r=n>0?"top":"bottom",i="center"):Dr(o-Uw)?(r=n>0?"bottom":"top",i="center"):(r="middle",i=o>0&&o<Uw?n>0?"right":"left":n>0?"left":"right"),{rotation:o,textAlign:i,textVerticalAlign:r}},qw=d,$w=v,Kw=as({type:"axis",_axisPointer:null,axisPointerClass:null,render:function(t,e,n,i){this.axisPointerClass&&Ou(t),Kw.superApply(this,"render",arguments),Bu(this,t,0,n,0,!0)},updateAxisPointer:function(t,e,n,i,r){Bu(this,t,0,n,0,!1)},remove:function(t,e){var n=this._axisPointer;n&&n.remove(e),Kw.superApply(this,"remove",arguments)},dispose:function(t,e){Vu(this,e),Kw.superApply(this,"dispose",arguments)}}),Qw=[];Kw.registerAxisPointerClass=function(t,e){Qw[t]=e},Kw.getAxisPointerClass=function(t){return t&&Qw[t]};var Jw=["axisLine","axisTickLabel","axisName"],tb=["splitArea","splitLine"],eb=Kw.extend({type:"cartesianAxis",axisPointerClass:"CartesianAxisPointer",render:function(t,e,n,i){this.group.removeAll();var r=this._axisGroup;if(this._axisGroup=new Sg,this.group.add(this._axisGroup),t.get("show")){var o=t.getCoordSysModel(),a=Fu(o,t),s=new Xw(t,a);d(Jw,s.add,s),this._axisGroup.add(s.getGroup()),d(tb,function(e){t.get(e+".show")&&this["_"+e](t,o)},this),cr(r,this._axisGroup,t),eb.superCall(this,"render",t,e,n,i)}},remove:function(){this._splitAreaColors=null},_splitLine:function(t,e){var n=t.axis;if(!n.scale.isBlank()){var i=t.getModel("splitLine"),r=i.getModel("lineStyle"),o=r.get("color");o=y(o)?o:[o];for(var s=e.coordinateSystem.getRect(),l=n.isHorizontal(),u=0,h=n.getTicksCoords({tickModel:i}),c=[],d=[],f=r.getLineStyle(),p=0;p<h.length;p++){var g=n.toGlobalCoord(h[p].coord);l?(c[0]=g,c[1]=s.y,d[0]=g,d[1]=s.y+s.height):(c[0]=s.x,c[1]=g,d[0]=s.x+s.width,d[1]=g);var m=u++%o.length,v=h[p].tickValue;this._axisGroup.add(new Hv(zi({anid:null!=v?"line_"+h[p].tickValue:null,shape:{x1:c[0],y1:c[1],x2:d[0],y2:d[1]},style:a({stroke:o[m]},f),silent:!0})))}}},_splitArea:function(t,e){var n=t.axis;if(!n.scale.isBlank()){var i=t.getModel("splitArea"),r=i.getModel("areaStyle"),o=r.get("color"),s=e.coordinateSystem.getRect(),l=n.getTicksCoords({tickModel:i,clamp:!0});if(l.length){var u=o.length,h=this._splitAreaColors,c=N(),d=0;if(h)for(m=0;m<l.length;m++){var f=h.get(l[m].tickValue);if(null!=f){d=(f+(u-1)*m)%u;break}}var p=n.toGlobalCoord(l[0].coord),g=r.getAreaStyle();o=y(o)?o:[o];for(var m=1;m<l.length;m++){var v,x,_,w,b=n.toGlobalCoord(l[m].coord);n.isHorizontal()?(v=p,x=s.y,_=b-v,w=s.height,p=v+_):(v=s.x,x=p,_=s.width,p=x+(w=b-x));var M=l[m-1].tickValue;null!=M&&c.set(M,d),this._axisGroup.add(new Fv({anid:null!=M?"area_"+M:null,shape:{x:v,y:x,width:_,height:w},style:a({fill:o[d]},g),silent:!0})),d=(d+1)%u}this._splitAreaColors=c}}}});eb.extend({type:"xAxis"}),eb.extend({type:"yAxis"}),as({type:"grid",render:function(t,e){this.group.removeAll(),t.get("show")&&this.group.add(new Fv({shape:t.coordinateSystem.getRect(),style:a({fill:t.get("backgroundColor")},t.getItemStyle()),silent:!0,z2:-1}))}}),Qa(function(t){t.xAxis&&t.yAxis&&!t.grid&&(t.grid={})}),ns(Pw("line","circle","line")),es(Lw("line")),Ja(Xx.PROCESSOR.STATISTIC,function(t){return{seriesType:t,modifyOutputEnd:!0,reset:function(t,e,n){var i=t.getData(),r=t.get("sampling"),o=t.coordinateSystem;if("cartesian2d"===o.type&&r){var a=o.getBaseAxis(),s=o.getOtherAxis(a),l=a.getExtent(),u=l[1]-l[0],h=Math.round(i.count()/u);if(h>1){var c;"string"==typeof r?c=Ow[r]:"function"==typeof r&&(c=r),c&&t.setData(i.downSample(i.mapDimension(s.dim),1/h,c,zw))}}}}}("line")),cx.extend({type:"series.__base_bar__",getInitialData:function(t,e){return Os(this.getSource(),this)},getMarkerPosition:function(t){var e=this.coordinateSystem;if(e){var n=e.dataToPoint(e.clampData(t)),i=this.getData(),r=i.getLayout("offset"),o=i.getLayout("size");return n[e.getBaseAxis().isHorizontal()?0:1]+=r+o/2,n}return[NaN,NaN]},defaultOption:{zlevel:0,z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod",itemStyle:{},emphasis:{}}}).extend({type:"series.bar",dependencies:["grid","polar"],brushSelector:"rect",getProgressive:function(){return!!this.get("large")&&this.get("progressive")},getProgressiveThreshold:function(){var t=this.get("progressiveThreshold"),e=this.get("largeThreshold");return e>t&&(t=e),t}});var nb=Sm([["fill","color"],["stroke","borderColor"],["lineWidth","borderWidth"],["stroke","barBorderColor"],["lineWidth","barBorderWidth"],["opacity"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["shadowColor"]]),ib={getBarItemStyle:function(t){var e=nb(this,t);if(this.getBorderLineDash){var n=this.getBorderLineDash();n&&(e.lineDash=n)}return e}},rb=["itemStyle","barBorderWidth"];o(pr.prototype,ib),ls({type:"bar",render:function(t,e,n){this._updateDrawMode(t);var i=t.get("coordinateSystem");return"cartesian2d"!==i&&"polar"!==i||(this._isLargeDraw?this._renderLarge(t,e,n):this._renderNormal(t,e,n)),this.group},incrementalPrepareRender:function(t,e,n){this._clear(),this._updateDrawMode(t)},incrementalRender:function(t,e,n,i){this._incrementalRenderLarge(t,e)},_updateDrawMode:function(t){var e=t.pipelineContext.large;(null==this._isLargeDraw||e^this._isLargeDraw)&&(this._isLargeDraw=e,this._clear())},_renderNormal:function(t,e,n){var i,r=this.group,o=t.getData(),a=this._data,s=t.coordinateSystem,l=s.getBaseAxis();"cartesian2d"===s.type?i=l.isHorizontal():"polar"===s.type&&(i="angle"===l.dim);var u=t.isAnimationEnabled()?t:null;o.diff(a).add(function(e){if(o.hasValue(e)){var n=o.getItemModel(e),a=ab[s.type](o,e,n),l=ob[s.type](o,e,n,a,i,u);o.setItemGraphicEl(e,l),r.add(l),Uu(l,o,e,n,a,t,i,"polar"===s.type)}}).update(function(e,n){var l=a.getItemGraphicEl(n);if(o.hasValue(e)){var h=o.getItemModel(e),c=ab[s.type](o,e,h);l?ar(l,{shape:c},u,e):l=ob[s.type](o,e,h,c,i,u,!0),o.setItemGraphicEl(e,l),r.add(l),Uu(l,o,e,h,c,t,i,"polar"===s.type)}else r.remove(l)}).remove(function(t){var e=a.getItemGraphicEl(t);"cartesian2d"===s.type?e&&Wu(t,u,e):e&&Zu(t,u,e)}).execute(),this._data=o},_renderLarge:function(t,e,n){this._clear(),ju(t,this.group)},_incrementalRenderLarge:function(t,e){ju(e,this.group,!0)},dispose:R,remove:function(t){this._clear(t)},_clear:function(t){var e=this.group,n=this._data;t&&t.get("animation")&&n&&!this._isLargeDraw?n.eachItemGraphicEl(function(e){"sector"===e.type?Zu(e.dataIndex,t,e):Wu(e.dataIndex,t,e)}):e.removeAll(),this._data=null}});var ob={cartesian2d:function(t,e,n,i,r,a,s){var l=new Fv({shape:o({},i)});if(a){var u=l.shape,h=r?"height":"width",c={};u[h]=0,c[h]=i[h],ty[s?"updateProps":"initProps"](l,{shape:c},a,e)}return l},polar:function(t,e,n,i,r,o,s){var l=i.startAngle<i.endAngle,u=new zv({shape:a({clockwise:l},i)});if(o){var h=u.shape,c=r?"r":"endAngle",d={};h[c]=r?0:i.startAngle,d[c]=i[c],ty[s?"updateProps":"initProps"](u,{shape:d},o,e)}return u}},ab={cartesian2d:function(t,e,n){var i=t.getItemLayout(e),r=Xu(n,i),o=i.width>0?1:-1,a=i.height>0?1:-1;return{x:i.x+o*r/2,y:i.y+a*r/2,width:i.width-o*r,height:i.height-a*r}},polar:function(t,e,n){var i=t.getItemLayout(e);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle}}},sb=xi.extend({type:"largeBar",shape:{points:[]},buildPath:function(t,e){for(var n=e.points,i=this.__startPoint,r=this.__valueIdx,o=0;o<n.length;o+=2)i[this.__valueIdx]=n[o+r],t.moveTo(i[0],i[1]),t.lineTo(n[o],n[o+1])}});es(v(function(t,e){var n=js(t,e),i=Ys(n),r={};d(n,function(t){var e=t.getData(),n=t.coordinateSystem,o=n.getBaseAxis(),a=Us(t),s=i[Xs(o)][a],l=s.offset,u=s.width,h=n.getOtherAxis(o),c=t.get("barMinHeight")||0;r[a]=r[a]||[],e.setLayout({offset:l,size:u});for(var d=e.mapDimension(h.dim),f=e.mapDimension(o.dim),p=Ps(e,d),g=h.isHorizontal(),m=Js(o,h,p),v=0,y=e.count();v<y;v++){var x=e.get(d,v),_=e.get(f,v);if(!isNaN(x)){var w=x>=0?"p":"n",b=m;p&&(r[a][_]||(r[a][_]={p:m,n:m}),b=r[a][_][w]);var M,S,I,C;if(g)M=b,S=(T=n.dataToPoint([x,_]))[1]+l,I=T[0]-m,C=u,Math.abs(I)<c&&(I=(I<0?-1:1)*c),p&&(r[a][_][w]+=I);else{var T=n.dataToPoint([_,x]);M=T[0]+l,S=b,I=u,C=T[1]-m,Math.abs(C)<c&&(C=(C<=0?-1:1)*c),p&&(r[a][_][w]+=C)}e.setItemLayout(v,{x:M,y:S,width:I,height:C})}}},this)},"bar")),es(E_),ns({seriesType:"bar",reset:function(t){t.getData().setVisual("legendSymbol","roundRect")}});var lb=function(t,e,n){e=y(e)&&{coordDimensions:e}||o({},e);var i=t.getSource(),r=C_(i,e),a=new M_(r,t);return a.initData(i,n),a},ub={updateSelectedMap:function(t){this._targetList=y(t)?t.slice():[],this._selectTargetMap=p(t||[],function(t,e){return t.set(e.name,e),t},N())},select:function(t,e){var n=null!=e?this._targetList[e]:this._selectTargetMap.get(t);"single"===this.get("selectedMode")&&this._selectTargetMap.each(function(t){t.selected=!1}),n&&(n.selected=!0)},unSelect:function(t,e){var n=null!=e?this._targetList[e]:this._selectTargetMap.get(t);n&&(n.selected=!1)},toggleSelected:function(t,e){var n=null!=e?this._targetList[e]:this._selectTargetMap.get(t);if(null!=n)return this[n.selected?"unSelect":"select"](t,e),n.selected},isSelected:function(t,e){var n=null!=e?this._targetList[e]:this._selectTargetMap.get(t);return n&&n.selected}},hb=ss({type:"series.pie",init:function(t){hb.superApply(this,"init",arguments),this.legendDataProvider=function(){return this.getRawData()},this.updateSelectedMap(this._createSelectableList()),this._defaultLabelLine(t)},mergeOption:function(t){hb.superCall(this,"mergeOption",t),this.updateSelectedMap(this._createSelectableList())},getInitialData:function(t,e){return lb(this,["value"])},_createSelectableList:function(){for(var t=this.getRawData(),e=t.mapDimension("value"),n=[],i=0,r=t.count();i<r;i++)n.push({name:t.getName(i),value:t.get(e,i),selected:Uo(t,i,"selected")});return n},getDataParams:function(t){var e=this.getData(),n=hb.superCall(this,"getDataParams",t),i=[];return e.each(e.mapDimension("value"),function(t){i.push(t)}),n.percent=Cr(i,t,e.hostModel.get("percentPrecision")),n.$vars.push("percent"),n},_defaultLabelLine:function(t){_n(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},defaultOption:{zlevel:0,z:2,legendHoverLink:!0,hoverAnimation:!0,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,selectedOffset:10,hoverOffset:10,avoidLabelOverlap:!0,percentPrecision:2,stillShowZeroSum:!0,label:{rotate:!1,show:!0,position:"outer"},labelLine:{show:!0,length:15,length2:15,smooth:!1,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1},animationType:"expansion",animationEasing:"cubicOut"}});h(hb,ub);var cb=Ku.prototype;cb.updateData=function(t,e,n){function i(){s.stopAnimation(!0),s.animateTo({shape:{r:h.r+l.get("hoverOffset")}},300,"elasticOut")}function r(){s.stopAnimation(!0),s.animateTo({shape:{r:h.r}},300,"elasticOut")}var s=this.childAt(0),l=t.hostModel,u=t.getItemModel(e),h=t.getItemLayout(e),c=o({},h);c.label=null,n?(s.setShape(c),"scale"===l.getShallow("animationType")?(s.shape.r=h.r0,sr(s,{shape:{r:h.r}},l,e)):(s.shape.endAngle=h.startAngle,ar(s,{shape:{endAngle:h.endAngle}},l,e))):ar(s,{shape:c},l,e);var d=t.getItemVisual(e,"color");s.useStyle(a({lineJoin:"bevel",fill:d},u.getModel("itemStyle").getItemStyle())),s.hoverStyle=u.getModel("emphasis.itemStyle").getItemStyle();var f=u.getShallow("cursor");f&&s.attr("cursor",f),$u(this,t.getItemLayout(e),l.isSelected(null,e),l.get("selectedOffset"),l.get("animation")),s.off("mouseover").off("mouseout").off("emphasis").off("normal"),u.get("hoverAnimation")&&l.isAnimationEnabled()&&s.on("mouseover",i).on("mouseout",r).on("emphasis",i).on("normal",r),this._updateLabel(t,e),qi(this)},cb._updateLabel=function(t,e){var n=this.childAt(1),i=this.childAt(2),r=t.hostModel,o=t.getItemModel(e),a=t.getItemLayout(e).label,s=t.getItemVisual(e,"color");ar(n,{shape:{points:a.linePoints||[[a.x,a.y],[a.x,a.y],[a.x,a.y]]}},r,e),ar(i,{style:{x:a.x,y:a.y}},r,e),i.attr({rotation:a.rotation,origin:[a.x,a.y],z2:10});var l=o.getModel("label"),u=o.getModel("emphasis.label"),h=o.getModel("labelLine"),c=o.getModel("emphasis.labelLine"),s=t.getItemVisual(e,"color");$i(i.style,i.hoverStyle={},l,u,{labelFetcher:t.hostModel,labelDataIndex:e,defaultText:t.getName(e),autoColor:s,useInsideStyle:!!a.inside},{textAlign:a.textAlign,textVerticalAlign:a.verticalAlign,opacity:t.getItemVisual(e,"opacity")}),i.ignore=i.normalIgnore=!l.get("show"),i.hoverIgnore=!u.get("show"),n.ignore=n.normalIgnore=!h.get("show"),n.hoverIgnore=!c.get("show"),n.setStyle({stroke:s,opacity:t.getItemVisual(e,"opacity")}),n.setStyle(h.getModel("lineStyle").getLineStyle()),n.hoverStyle=c.getModel("lineStyle").getLineStyle();var d=h.get("smooth");d&&!0===d&&(d=.4),n.setShape({smooth:d})},u(Ku,Sg);ra.extend({type:"pie",init:function(){var t=new Sg;this._sectorGroup=t},render:function(t,e,n,i){if(!i||i.from!==this.uid){var r=t.getData(),o=this._data,a=this.group,s=e.get("animation"),l=!o,u=t.get("animationType"),h=v(qu,this.uid,t,s,n),c=t.get("selectedMode");if(r.diff(o).add(function(t){var e=new Ku(r,t);l&&"scale"!==u&&e.eachChild(function(t){t.stopAnimation(!0)}),c&&e.on("click",h),r.setItemGraphicEl(t,e),a.add(e)}).update(function(t,e){var n=o.getItemGraphicEl(e);n.updateData(r,t),n.off("click"),c&&n.on("click",h),a.add(n),r.setItemGraphicEl(t,n)}).remove(function(t){var e=o.getItemGraphicEl(t);a.remove(e)}).execute(),s&&l&&r.count()>0&&"scale"!==u){var d=r.getItemLayout(0),f=Math.max(n.getWidth(),n.getHeight())/2,p=m(a.removeClipPath,a);a.setClipPath(this._createClipPath(d.cx,d.cy,f,d.startAngle,d.clockwise,p,t))}this._data=r}},dispose:function(){},_createClipPath:function(t,e,n,i,r,o,a){var s=new zv({shape:{cx:t,cy:e,r0:0,r:n,startAngle:i,endAngle:i,clockwise:r}});return sr(s,{shape:{endAngle:i+(r?1:-1)*Math.PI*2}},a,o),s},containPoint:function(t,e){var n=e.getData().getItemLayout(0);if(n){var i=t[0]-n.cx,r=t[1]-n.cy,o=Math.sqrt(i*i+r*r);return o<=n.r&&o>=n.r0}}});var db=function(t,e,n,i){var r,o,a=t.getData(),s=[],l=!1;a.each(function(n){var i,u,h,c,d=a.getItemLayout(n),f=a.getItemModel(n),p=f.getModel("label"),g=p.get("position")||f.get("emphasis.label.position"),m=f.getModel("labelLine"),v=m.get("length"),y=m.get("length2"),x=(d.startAngle+d.endAngle)/2,_=Math.cos(x),w=Math.sin(x);r=d.cx,o=d.cy;var b="inside"===g||"inner"===g;if("center"===g)i=d.cx,u=d.cy,c="center";else{var M=(b?(d.r+d.r0)/2*_:d.r*_)+r,S=(b?(d.r+d.r0)/2*w:d.r*w)+o;if(i=M+3*_,u=S+3*w,!b){var I=M+_*(v+e-d.r),C=S+w*(v+e-d.r),T=I+(_<0?-1:1)*y,D=C;i=T+(_<0?-5:5),u=D,h=[[M,S],[I,C],[T,D]]}c=b?"center":_>0?"left":"right"}var A=p.getFont(),k=p.get("rotate")?_<0?-x+Math.PI:-x:0,P=ce(t.getFormattedLabel(n,"normal")||a.getName(n),A,c,"top");l=!!k,d.label={x:i,y:u,position:g,height:P.height,len:v,len2:y,linePoints:h,textAlign:c,verticalAlign:"middle",rotation:k,inside:b},b||s.push(d.label)}),!l&&t.get("avoidLabelOverlap")&&Ju(s,r,o,e,n,i)},fb=2*Math.PI,pb=Math.PI/180;!function(t,e){d(e,function(e){e.update="updateView",ts(e,function(n,i){var r={};return i.eachComponent({mainType:"series",subType:t,query:n},function(t){t[e.method]&&t[e.method](n.name,n.dataIndex);var i=t.getData();i.each(function(e){var n=i.getName(e);r[n]=t.isSelected(n)||!1})}),{name:n.name,selected:r}})})}("pie",[{type:"pieToggleSelect",event:"pieselectchanged",method:"toggleSelected"},{type:"pieSelect",event:"pieselected",method:"select"},{type:"pieUnSelect",event:"pieunselected",method:"unSelect"}]),ns(function(t){return{getTargetSeries:function(e){var n={},i=N();return e.eachSeriesByType(t,function(t){t.__paletteScope=n,i.set(t.uid,t)}),i},reset:function(t,e){var n=t.getRawData(),i={},r=t.getData();r.each(function(t){var e=r.getRawIndex(t);i[e]=t}),n.each(function(e){var o=i[e],a=null!=o&&r.getItemVisual(o,"color",!0);if(a)n.setItemVisual(e,"color",a);else{var s=n.getItemModel(e).get("itemStyle.color")||t.getColorFromPalette(n.getName(e)||e+"",t.__paletteScope,n.count());n.setItemVisual(e,"color",s),null!=o&&r.setItemVisual(o,"color",s)}})}}}("pie")),es(v(function(t,e,n,i){e.eachSeriesByType(t,function(t){var e=t.getData(),i=e.mapDimension("value"),r=t.get("center"),o=t.get("radius");y(o)||(o=[0,o]),y(r)||(r=[r,r]);var a=n.getWidth(),s=n.getHeight(),l=Math.min(a,s),u=_r(r[0],a),h=_r(r[1],s),c=_r(o[0],l/2),d=_r(o[1],l/2),f=-t.get("startAngle")*pb,p=t.get("minAngle")*pb,g=0;e.each(i,function(t){!isNaN(t)&&g++});var m=e.getSum(i),v=Math.PI/(m||g)*2,x=t.get("clockwise"),_=t.get("roseType"),w=t.get("stillShowZeroSum"),b=e.getDataExtent(i);b[0]=0;var M=fb,S=0,I=f,C=x?1:-1;if(e.each(i,function(t,n){var i;if(isNaN(t))e.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:x,cx:u,cy:h,r0:c,r:_?NaN:d});else{(i="area"!==_?0===m&&w?v:t*v:fb/g)<p?(i=p,M-=p):S+=t;var r=I+C*i;e.setItemLayout(n,{angle:i,startAngle:I,endAngle:r,clockwise:x,cx:u,cy:h,r0:c,r:_?xr(t,b,[c,d]):d}),I=r}}),M<fb&&g)if(M<=.001){var T=fb/g;e.each(i,function(t,n){if(!isNaN(t)){var i=e.getItemLayout(n);i.angle=T,i.startAngle=f+C*n*T,i.endAngle=f+C*(n+1)*T}})}else v=M/S,I=f,e.each(i,function(t,n){if(!isNaN(t)){var i=e.getItemLayout(n),r=i.angle===p?p:t*v;i.startAngle=I,i.endAngle=I+C*r,I+=C*r}});db(t,d,a,s)})},"pie")),Ja(function(t){return{seriesType:t,reset:function(t,e){var n=e.findComponents({mainType:"legend"});if(n&&n.length){var i=t.getData();i.filterSelf(function(t){for(var e=i.getName(t),r=0;r<n.length;r++)if(!n[r].isSelected(e))return!1;return!0})}}}}("pie")),cx.extend({type:"series.scatter",dependencies:["grid","polar","geo","singleAxis","calendar"],getInitialData:function(t,e){return Os(this.getSource(),this)},brushSelector:"point",getProgressive:function(){var t=this.option.progressive;return null==t?this.option.large?5e3:this.get("progressive"):t},getProgressiveThreshold:function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?1e4:this.get("progressiveThreshold"):t},defaultOption:{coordinateSystem:"cartesian2d",zlevel:0,z:2,legendHoverLink:!0,hoverAnimation:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8}}});var gb=Ai({shape:{points:null},symbolProxy:null,buildPath:function(t,e){var n=e.points,i=e.size,r=this.symbolProxy,o=r.shape;if(!((t.getContext?t.getContext():t)&&i[0]<4))for(var a=0;a<n.length;){var s=n[a++],l=n[a++];isNaN(s)||isNaN(l)||(o.x=s-i[0]/2,o.y=l-i[1]/2,o.width=i[0],o.height=i[1],r.buildPath(t,o,!0))}},afterBrush:function(t){var e=this.shape,n=e.points,i=e.size;if(i[0]<4){this.setTransform(t);for(var r=0;r<n.length;){var o=n[r++],a=n[r++];isNaN(o)||isNaN(a)||t.fillRect(o-i[0]/2,a-i[1]/2,i[0],i[1])}this.restoreTransform(t)}},findDataIndex:function(t,e){for(var n=this.shape,i=n.points,r=n.size,o=Math.max(r[0],4),a=Math.max(r[1],4),s=i.length/2-1;s>=0;s--){var l=2*s,u=i[l]-o/2,h=i[l+1]-a/2;if(t>=u&&e>=h&&t<=u+o&&e<=h+a)return s}return-1}}),mb=th.prototype;mb.isPersistent=function(){return!this._incremental},mb.updateData=function(t){this.group.removeAll();var e=new gb({rectHover:!0,cursor:"default"});e.setShape({points:t.getLayout("symbolPoints")}),this._setCommon(e,t),this.group.add(e),this._incremental=null},mb.updateLayout=function(t){if(!this._incremental){var e=t.getLayout("symbolPoints");this.group.eachChild(function(t){if(null!=t.startIndex){var n=2*(t.endIndex-t.startIndex),i=4*t.startIndex*2;e=new Float32Array(e.buffer,i,n)}t.setShape("points",e)})}},mb.incrementalPrepareUpdate=function(t){this.group.removeAll(),this._clearIncremental(),t.count()>2e6?(this._incremental||(this._incremental=new Di({silent:!0})),this.group.add(this._incremental)):this._incremental=null},mb.incrementalUpdate=function(t,e){var n;this._incremental?(n=new gb,this._incremental.addDisplayable(n,!0)):((n=new gb({rectHover:!0,cursor:"default",startIndex:t.start,endIndex:t.end})).incremental=!0,this.group.add(n)),n.setShape({points:e.getLayout("symbolPoints")}),this._setCommon(n,e,!!this._incremental)},mb._setCommon=function(t,e,n){var i=e.hostModel,r=e.getVisual("symbolSize");t.setShape("size",r instanceof Array?r:[r,r]),t.symbolProxy=cl(e.getVisual("symbol"),0,0,0,0),t.setColor=t.symbolProxy.setColor;var o=t.shape.size[0]<4;t.useStyle(i.getModel("itemStyle").getItemStyle(o?["color","shadowBlur","shadowColor"]:["color"]));var a=e.getVisual("color");a&&t.setColor(a),n||(t.seriesIndex=i.seriesIndex,t.on("mousemove",function(e){t.dataIndex=null;var n=t.findDataIndex(e.offsetX,e.offsetY);n>=0&&(t.dataIndex=n+(t.startIndex||0))}))},mb.remove=function(){this._clearIncremental(),this._incremental=null,this.group.removeAll()},mb._clearIncremental=function(){var t=this._incremental;t&&t.clearDisplaybles()},ls({type:"scatter",render:function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).updateData(i),this._finished=!0},incrementalPrepareRender:function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).incrementalPrepareUpdate(i),this._finished=!1},incrementalRender:function(t,e,n){this._symbolDraw.incrementalUpdate(t,e.getData()),this._finished=t.end===e.getData().count()},updateTransform:function(t,e,n){var i=t.getData();if(this.group.dirty(),!this._finished||i.count()>1e4||!this._symbolDraw.isPersistent())return{update:!0};var r=Lw().reset(t);r.progress&&r.progress({start:0,end:i.count()},i),this._symbolDraw.updateLayout(i)},_updateSymbolDraw:function(t,e){var n=this._symbolDraw,i=e.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new th:new Bl,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},remove:function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},dispose:function(){}}),ns(Pw("scatter","circle")),es(Lw("scatter")),Qa(function(t){var e=t.graphic;y(e)?e[0]&&e[0].elements?t.graphic=[t.graphic[0]]:t.graphic=[{elements:e}]:e&&!e.elements&&(t.graphic=[{elements:[e]}])});var vb=os({type:"graphic",defaultOption:{elements:[],parentId:null},_elOptionsToUpdate:null,mergeOption:function(t){var e=this.option.elements;this.option.elements=null,vb.superApply(this,"mergeOption",arguments),this.option.elements=e},optionUpdated:function(t,e){var n=this.option,i=(e?n:t).elements,r=n.elements=e?[]:n.elements,o=[];this._flatten(i,o);var a=Mn(r,o);Sn(a);var s=this._elOptionsToUpdate=[];d(a,function(t,e){var n=t.option;n&&(s.push(n),oh(t,n),ah(r,e,n),sh(r[e],n))},this);for(var l=r.length-1;l>=0;l--)null==r[l]?r.splice(l,1):delete r[l].$action},_flatten:function(t,e,n){d(t,function(t){if(t){n&&(t.parentOption=n),e.push(t);var i=t.children;"group"===t.type&&i&&this._flatten(i,e,t),delete t.children}},this)},useElOptionsToUpdate:function(){var t=this._elOptionsToUpdate;return this._elOptionsToUpdate=null,t}});as({type:"graphic",init:function(t,e){this._elMap=N(),this._lastGraphicModel},render:function(t,e,n){t!==this._lastGraphicModel&&this._clear(),this._lastGraphicModel=t,this._updateElements(t,n),this._relocate(t,n)},_updateElements:function(t,e){var n=t.useElOptionsToUpdate();if(n){var i=this._elMap,r=this.group;d(n,function(t){var e=t.$action,n=t.id,o=i.get(n),a=t.parentId,s=null!=a?i.get(a):r;if("text"===t.type){var l=t.style;t.hv&&t.hv[1]&&(l.textVerticalAlign=l.textBaseline=null),!l.hasOwnProperty("textFill")&&l.fill&&(l.textFill=l.fill),!l.hasOwnProperty("textStroke")&&l.stroke&&(l.textStroke=l.stroke)}var u=ih(t);e&&"merge"!==e?"replace"===e?(nh(o,i),eh(n,s,u,i)):"remove"===e&&nh(o,i):o?o.attr(u):eh(n,s,u,i);var h=i.get(n);h&&(h.__ecGraphicWidth=t.width,h.__ecGraphicHeight=t.height)})}},_relocate:function(t,e){for(var n=t.option.elements,i=this.group,r=this._elMap,o=n.length-1;o>=0;o--){var a=n[o],s=r.get(a.id);if(s){var l=s.parent;Wr(s,a,l===i?{width:e.getWidth(),height:e.getHeight()}:{width:l.__ecGraphicWidth||0,height:l.__ecGraphicHeight||0},null,{hv:a.hv,boundingMode:a.bounding})}}},_clear:function(){var t=this._elMap;t.each(function(e){nh(e,t)}),this._elMap=N()},dispose:function(){this._clear()}});var yb=function(t,e){var n,i=[],r=t.seriesIndex;if(null==r||!(n=e.getSeriesByIndex(r)))return{point:[]};var o=n.getData(),a=Tn(o,t);if(null==a||a<0||y(a))return{point:[]};var s=o.getItemGraphicEl(a),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(a)||[];else if(l&&l.dataToPoint)i=l.dataToPoint(o.getValues(f(l.dimensions,function(t){return o.mapDimension(t)}),a,!0))||[];else if(s){var u=s.getBoundingRect().clone();u.applyTransform(s.transform),i=[u.x+u.width/2,u.y+u.height/2]}return{point:i,el:s}},xb=d,_b=v,wb=Dn(),bb=(os({type:"axisPointer",coordSysAxesInfo:null,defaultOption:{show:"auto",triggerOn:null,zlevel:0,z:50,type:"line",snap:!1,triggerTooltip:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#aaa",width:1,type:"solid"},shadowStyle:{color:"rgba(150,150,150,0.3)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,shadowBlur:3,shadowColor:"#aaa"},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}}}),Dn()),Mb=d,Sb=as({type:"axisPointer",render:function(t,e,n){var i=e.getComponent("tooltip"),r=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";yh("axisPointer",n,function(t,e,n){"none"!==r&&("leave"===t||r.indexOf(t)>=0)&&n({type:"updateAxisPointer",currTrigger:t,x:e&&e.offsetX,y:e&&e.offsetY})})},remove:function(t,e){Sh(e.getZr(),"axisPointer"),Sb.superApply(this._model,"remove",arguments)},dispose:function(t,e){Sh("axisPointer",e),Sb.superApply(this._model,"dispose",arguments)}}),Ib=Dn(),Cb=n,Tb=m;(Ih.prototype={_group:null,_lastGraphicKey:null,_handle:null,_dragging:!1,_lastValue:null,_lastStatus:null,_payloadInfo:null,animationThreshold:15,render:function(t,e,n,i){var r=e.get("value"),o=e.get("status");if(this._axisModel=t,this._axisPointerModel=e,this._api=n,i||this._lastValue!==r||this._lastStatus!==o){this._lastValue=r,this._lastStatus=o;var a=this._group,s=this._handle;if(!o||"hide"===o)return a&&a.hide(),void(s&&s.hide());a&&a.show(),s&&s.show();var l={};this.makeElOption(l,r,t,e,n);var u=l.graphicKey;u!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=u;var h=this._moveAnimation=this.determineAnimation(t,e);if(a){var c=v(Ch,e,h);this.updatePointerEl(a,l,c,e),this.updateLabelEl(a,l,c,e)}else a=this._group=new Sg,this.createPointerEl(a,l,t,e),this.createLabelEl(a,l,t,e),n.getZr().add(a);kh(a,e,!0),this._renderHandle(r)}},remove:function(t){this.clear(t)},dispose:function(t){this.clear(t)},determineAnimation:function(t,e){var n=e.get("animation"),i=t.axis,r="category"===i.type,o=e.get("snap");if(!o&&!r)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(r&&i.getBandWidth()>a)return!0;if(o){var s=zu(t).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>a}return!1}return!0===n},makeElOption:function(t,e,n,i,r){},createPointerEl:function(t,e,n,i){var r=e.pointer;if(r){var o=Ib(t).pointerEl=new ty[r.type](Cb(e.pointer));t.add(o)}},createLabelEl:function(t,e,n,i){if(e.label){var r=Ib(t).labelEl=new Fv(Cb(e.label));t.add(r),Dh(r,i)}},updatePointerEl:function(t,e,n){var i=Ib(t).pointerEl;i&&(i.setStyle(e.pointer.style),n(i,{shape:e.pointer.shape}))},updateLabelEl:function(t,e,n,i){var r=Ib(t).labelEl;r&&(r.setStyle(e.label.style),n(r,{shape:e.label.shape,position:e.label.position}),Dh(r,i))},_renderHandle:function(t){if(!this._dragging&&this.updateHandleTransform){var e=this._axisPointerModel,n=this._api.getZr(),i=this._handle,r=e.getModel("handle"),o=e.get("status");if(!r.get("show")||!o||"hide"===o)return i&&n.remove(i),void(this._handle=null);var a;this._handle||(a=!0,i=this._handle=fr(r.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(t){tm(t.event)},onmousedown:Tb(this._onHandleDragMove,this,0,0),drift:Tb(this._onHandleDragMove,this),ondragend:Tb(this._onHandleDragEnd,this)}),n.add(i)),kh(i,e,!1);var s=["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"];i.setStyle(r.getItemStyle(null,s));var l=r.get("size");y(l)||(l=[l,l]),i.attr("scale",[l[0]/2,l[1]/2]),ha(this,"_doDispatchAxisPointer",r.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,a)}},_moveHandleToValue:function(t,e){Ch(this._axisPointerModel,!e&&this._moveAnimation,this._handle,Ah(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},_onHandleDragMove:function(t,e){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(Ah(n),[t,e],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(Ah(i)),Ib(n).lastProp=null,this._doDispatchAxisPointer()}},_doDispatchAxisPointer:function(){if(this._handle){var t=this._payloadInfo,e=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:e.axis.dim,axisIndex:e.componentIndex}]})}},_onHandleDragEnd:function(t){if(this._dragging=!1,this._handle){var e=this._axisPointerModel.get("value");this._moveHandleToValue(e),this._api.dispatchAction({type:"hideTip"})}},getHandleTransform:null,updateHandleTransform:null,clear:function(t){this._lastValue=null,this._lastStatus=null;var e=t.getZr(),n=this._group,i=this._handle;e&&n&&(this._lastGraphicKey=null,n&&e.remove(n),i&&e.remove(i),this._group=null,this._handle=null,this._payloadInfo=null)},doClear:function(){},buildLabel:function(t,e,n){return n=n||0,{x:t[n],y:t[1-n],width:e[n],height:e[1-n]}}}).constructor=Ih,En(Ih);var Db=Ih.extend({makeElOption:function(t,e,n,i,r){var o=n.axis,a=o.grid,s=i.get("type"),l=Vh(a,o).getOtherAxis(o).getGlobalExtent(),u=o.toGlobalCoord(o.dataToCoord(e,!0));if(s&&"none"!==s){var h=Ph(i),c=Ab[s](o,u,l,h);c.style=h,t.graphicKey=c.type,t.pointer=c}Nh(e,t,Fu(a.model,n),n,i,r)},getHandleTransform:function(t,e,n){var i=Fu(e.axis.grid.model,e,{labelInside:!1});return i.labelMargin=n.get("handle.margin"),{position:Eh(e.axis,t,i),rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},updateHandleTransform:function(t,e,n,i){var r=n.axis,o=r.grid,a=r.getGlobalExtent(!0),s=Vh(o,r).getOtherAxis(r).getGlobalExtent(),l="x"===r.dim?0:1,u=t.position;u[l]+=e[l],u[l]=Math.min(a[1],u[l]),u[l]=Math.max(a[0],u[l]);var h=(s[1]+s[0])/2,c=[h,h];c[l]=u[l];var d=[{verticalAlign:"middle"},{align:"center"}];return{position:u,rotation:t.rotation,cursorPoint:c,tooltipOption:d[l]}}}),Ab={line:function(t,e,n,i){var r=Rh([e,n[0]],[e,n[1]],Fh(t));return zi({shape:r,style:i}),{type:"Line",shape:r}},shadow:function(t,e,n,i){var r=Math.max(1,t.getBandWidth()),o=n[1]-n[0];return{type:"Rect",shape:Bh([e-r/2,n[0]],[r,o],Fh(t))}}};Kw.registerAxisPointerClass("CartesianAxisPointer",Db),Qa(function(t){if(t){(!t.axisPointer||0===t.axisPointer.length)&&(t.axisPointer={});var e=t.axisPointer.link;e&&!y(e)&&(t.axisPointer.link=[e])}}),Ja(Xx.PROCESSOR.STATISTIC,function(t,e){t.getComponent("axisPointer").coordSysAxesInfo=Tu(t,e)}),ts({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},function(t,e,n){var i=t.currTrigger,r=[t.x,t.y],o=t,a=t.dispatchAction||m(n.dispatchAction,n),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){vh(r)&&(r=yb({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},e).point);var l=vh(r),u=o.axesInfo,h=s.axesInfo,c="leave"===i||vh(r),d={},f={},p={list:[],map:{}},g={showPointer:_b(hh,f),showTooltip:_b(ch,p)};xb(s.coordSysMap,function(t,e){var n=l||t.containPoint(r);xb(s.coordSysAxesInfo[e],function(t,e){var i=t.axis,o=gh(u,t);if(!c&&n&&(!u||o)){var a=o&&o.value;null!=a||l||(a=i.pointToData(r)),null!=a&&lh(t,a,g,!1,d)}})});var v={};return xb(h,function(t,e){var n=t.linkGroup;n&&!f[e]&&xb(n.axesInfo,function(e,i){var r=f[i];if(e!==t&&r){var o=r.value;n.mapper&&(o=t.axis.scale.parse(n.mapper(o,mh(e),mh(t)))),v[t.key]=o}})}),xb(v,function(t,e){lh(h[e],t,g,!0,d)}),dh(f,h,d),fh(p,r,t,a),ph(h,0,n),d}}),os({type:"tooltip",dependencies:["axisPointer"],defaultOption:{zlevel:0,z:8,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",confine:!1,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"rgba(50,50,50,0.7)",borderColor:"#333",borderRadius:4,borderWidth:0,padding:5,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#fff",fontSize:14}}});var kb=d,Pb=zr,Lb=["","-webkit-","-moz-","-o-"];Zh.prototype={constructor:Zh,_enterable:!0,update:function(){var t=this._container,e=t.currentStyle||document.defaultView.getComputedStyle(t),n=t.style;"absolute"!==n.position&&"absolute"!==e.position&&(n.position="relative")},show:function(t){clearTimeout(this._hideTimeout);var e=this.el;e.style.cssText="position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;"+Wh(t)+";left:"+this._x+"px;top:"+this._y+"px;"+(t.get("extraCssText")||""),e.style.display=e.innerHTML?"block":"none",this._show=!0},setContent:function(t){this.el.innerHTML=null==t?"":t},setEnterable:function(t){this._enterable=t},getSize:function(){var t=this.el;return[t.clientWidth,t.clientHeight]},moveTo:function(t,e){var n,i=this._zr;i&&i.painter&&(n=i.painter.getViewportRootOffset())&&(t+=n.offsetLeft,e+=n.offsetTop);var r=this.el.style;r.left=t+"px",r.top=e+"px",this._x=t,this._y=e},hide:function(){this.el.style.display="none",this._show=!1},hideLater:function(t){!this._show||this._inContent&&this._enterable||(t?(this._hideDelay=t,this._show=!1,this._hideTimeout=setTimeout(m(this.hide,this),t)):this.hide())},isShow:function(){return this._show}};var Ob=m,zb=d,Eb=_r,Nb=new Fv({shape:{x:-1,y:-1,width:2,height:2}});as({type:"tooltip",init:function(t,e){if(!bp.node){var n=new Zh(e.getDom(),e);this._tooltipContent=n}},render:function(t,e,n){if(!bp.node&&!bp.wxa){this.group.removeAll(),this._tooltipModel=t,this._ecModel=e,this._api=n,this._lastDataByCoordSys=null,this._alwaysShowContent=t.get("alwaysShowContent");var i=this._tooltipContent;i.update(),i.setEnterable(t.get("enterable")),this._initGlobalListener(),this._keepShow()}},_initGlobalListener:function(){var t=this._tooltipModel.get("triggerOn");yh("itemTooltip",this._api,Ob(function(e,n,i){"none"!==t&&(t.indexOf(e)>=0?this._tryShow(n,i):"leave"===e&&this._hide(i))},this))},_keepShow:function(){var t=this._tooltipModel,e=this._ecModel,n=this._api;if(null!=this._lastX&&null!=this._lastY&&"none"!==t.get("triggerOn")){var i=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout(function(){i.manuallyShowTip(t,e,n,{x:i._lastX,y:i._lastY})})}},manuallyShowTip:function(t,e,n,i){if(i.from!==this.uid&&!bp.node){var r=Xh(i,n);this._ticket="";var o=i.dataByCoordSys;if(i.tooltip&&null!=i.x&&null!=i.y){var a=Nb;a.position=[i.x,i.y],a.update(),a.tooltip=i.tooltip,this._tryShow({offsetX:i.x,offsetY:i.y,target:a},r)}else if(o)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,event:{},dataByCoordSys:i.dataByCoordSys,tooltipOption:i.tooltipOption},r);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(t,e,n,i))return;var s=yb(i,e),l=s.point[0],u=s.point[1];null!=l&&null!=u&&this._tryShow({offsetX:l,offsetY:u,position:i.position,target:s.el,event:{}},r)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target,event:{}},r))}},manuallyHideTip:function(t,e,n,i){var r=this._tooltipContent;!this._alwaysShowContent&&this._tooltipModel&&r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=null,i.from!==this.uid&&this._hide(Xh(i,n))},_manuallyAxisShowTip:function(t,e,n,i){var r=i.seriesIndex,o=i.dataIndex,a=e.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=o&&null!=a){var s=e.getSeriesByIndex(r);if(s&&"axis"===(t=Uh([s.getData().getItemModel(o),s,(s.coordinateSystem||{}).model,t])).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:o,position:i.position}),!0}},_tryShow:function(t,e){var n=t.target;if(this._tooltipModel){this._lastX=t.offsetX,this._lastY=t.offsetY;var i=t.dataByCoordSys;i&&i.length?this._showAxisTooltip(i,t):n&&null!=n.dataIndex?(this._lastDataByCoordSys=null,this._showSeriesItemTooltip(t,n,e)):n&&n.tooltip?(this._lastDataByCoordSys=null,this._showComponentItemTooltip(t,n,e)):(this._lastDataByCoordSys=null,this._hide(e))}},_showOrMove:function(t,e){var n=t.get("showDelay");e=m(e,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(e,n):e()},_showAxisTooltip:function(t,e){var n=this._ecModel,i=this._tooltipModel,r=[e.offsetX,e.offsetY],o=[],a=[],s=Uh([e.tooltipOption,i]);zb(t,function(t){zb(t.dataByAxis,function(t){var e=n.getComponent(t.axisDim+"Axis",t.axisIndex),i=t.value,r=[];if(e&&null!=i){var s=zh(i,e.axis,n,t.seriesDataIndices,t.valueLabelOpt);d(t.seriesDataIndices,function(o){var l=n.getSeriesByIndex(o.seriesIndex),u=o.dataIndexInside,h=l&&l.getDataParams(u);h.axisDim=t.axisDim,h.axisIndex=t.axisIndex,h.axisType=t.axisType,h.axisId=t.axisId,h.axisValue=sl(e.axis,i),h.axisValueLabel=s,h&&(a.push(h),r.push(l.formatTooltip(u,!0)))});var l=s;o.push((l?Er(l)+"<br />":"")+r.join("<br />"))}})},this),o.reverse(),o=o.join("<br /><br />");var l=e.position;this._showOrMove(s,function(){this._updateContentNotChangedOnAxis(t)?this._updatePosition(s,l,r[0],r[1],this._tooltipContent,a):this._showTooltipContent(s,o,a,Math.random(),r[0],r[1],l)})},_showSeriesItemTooltip:function(t,e,n){var i=this._ecModel,r=e.seriesIndex,o=i.getSeriesByIndex(r),a=e.dataModel||o,s=e.dataIndex,l=e.dataType,u=a.getData(),h=Uh([u.getItemModel(s),a,o&&(o.coordinateSystem||{}).model,this._tooltipModel]),c=h.get("trigger");if(null==c||"item"===c){var d=a.getDataParams(s,l),f=a.formatTooltip(s,!1,l),p="item_"+a.name+"_"+s;this._showOrMove(h,function(){this._showTooltipContent(h,f,d,p,t.offsetX,t.offsetY,t.position,t.target)}),n({type:"showTip",dataIndexInside:s,dataIndex:u.getRawIndex(s),seriesIndex:r,from:this.uid})}},_showComponentItemTooltip:function(t,e,n){var i=e.tooltip;if("string"==typeof i){var r=i;i={content:r,formatter:r}}var o=new pr(i,this._tooltipModel,this._ecModel),a=o.get("content"),s=Math.random();this._showOrMove(o,function(){this._showTooltipContent(o,a,o.get("formatterParams")||{},s,t.offsetX,t.offsetY,t.position,e)}),n({type:"showTip",from:this.uid})},_showTooltipContent:function(t,e,n,i,r,o,a,s){if(this._ticket="",t.get("showContent")&&t.get("show")){var l=this._tooltipContent,u=t.get("formatter");a=a||t.get("position");var h=e;if(u&&"string"==typeof u)h=Nr(u,n,!0);else if("function"==typeof u){var c=Ob(function(e,i){e===this._ticket&&(l.setContent(i),this._updatePosition(t,a,r,o,l,n,s))},this);this._ticket=i,h=u(n,i,c)}l.setContent(h),l.show(t),this._updatePosition(t,a,r,o,l,n,s)}},_updatePosition:function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),h=t.get("align"),c=t.get("verticalAlign"),d=a&&a.getBoundingRect().clone();if(a&&d.applyTransform(a.transform),"function"==typeof e&&(e=e([n,i],o,r.el,d,{viewSize:[s,l],contentSize:u.slice()})),y(e))n=Eb(e[0],s),i=Eb(e[1],l);else if(w(e)){e.width=u[0],e.height=u[1];var f=Gr(e,{width:s,height:l});n=f.x,i=f.y,h=null,c=null}else"string"==typeof e&&a?(n=(p=$h(e,d,u))[0],i=p[1]):(n=(p=jh(n,i,r.el,s,l,h?null:20,c?null:20))[0],i=p[1]);if(h&&(n-=Kh(h)?u[0]/2:"right"===h?u[0]:0),c&&(i-=Kh(c)?u[1]/2:"bottom"===c?u[1]:0),t.get("confine")){var p=Yh(n,i,r.el,s,l);n=p[0],i=p[1]}r.moveTo(n,i)},_updateContentNotChangedOnAxis:function(t){var e=this._lastDataByCoordSys,n=!!e&&e.length===t.length;return n&&zb(e,function(e,i){var r=e.dataByAxis||{},o=(t[i]||{}).dataByAxis||[];(n&=r.length===o.length)&&zb(r,function(t,e){var i=o[e]||{},r=t.seriesDataIndices||[],a=i.seriesDataIndices||[];(n&=t.value===i.value&&t.axisType===i.axisType&&t.axisId===i.axisId&&r.length===a.length)&&zb(r,function(t,e){var i=a[e];n&=t.seriesIndex===i.seriesIndex&&t.dataIndex===i.dataIndex})})}),this._lastDataByCoordSys=t,!!n},_hide:function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},dispose:function(t,e){bp.node||bp.wxa||(this._tooltipContent.hide(),Sh("itemTooltip",e))}}),ts({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},function(){}),ts({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},function(){});var Rb=os({type:"legend.plain",dependencies:["series"],layoutMode:{type:"box",ignoreSize:!0},init:function(t,e,n){this.mergeDefaultAndTheme(t,n),t.selected=t.selected||{}},mergeOption:function(t){Rb.superCall(this,"mergeOption",t)},optionUpdated:function(){this._updateData(this.ecModel);var t=this._data;if(t[0]&&"single"===this.get("selectedMode")){for(var e=!1,n=0;n<t.length;n++){var i=t[n].get("name");if(this.isSelected(i)){this.select(i),e=!0;break}}!e&&this.select(t[0].get("name"))}},_updateData:function(t){var e=[],n=[];t.eachRawSeries(function(i){var r=i.name;n.push(r);var o;if(i.legendDataProvider){var a=i.legendDataProvider(),s=a.mapArray(a.getName);t.isSeriesFiltered(i)||(n=n.concat(s)),s.length?e=e.concat(s):o=!0}else o=!0;o&&In(i)&&e.push(i.name)}),this._availableNames=n;var i=f(this.get("data")||e,function(t){return"string"!=typeof t&&"number"!=typeof t||(t={name:t}),new pr(t,this,this.ecModel)},this);this._data=i},getData:function(){return this._data},select:function(t){var e=this.option.selected;"single"===this.get("selectedMode")&&d(this._data,function(t){e[t.get("name")]=!1}),e[t]=!0},unSelect:function(t){"single"!==this.get("selectedMode")&&(this.option.selected[t]=!1)},toggleSelected:function(t){var e=this.option.selected;e.hasOwnProperty(t)||(e[t]=!0),this[e[t]?"unSelect":"select"](t)},isSelected:function(t){var e=this.option.selected;return!(e.hasOwnProperty(t)&&!e[t])&&l(this._availableNames,t)>=0},defaultOption:{zlevel:0,z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,inactiveColor:"#ccc",textStyle:{color:"#333"},selectedMode:!0,tooltip:{show:!1}}});ts("legendToggleSelect","legendselectchanged",v(Qh,"toggleSelected")),ts("legendSelect","legendselected",v(Qh,"select")),ts("legendUnSelect","legendunselected",v(Qh,"unSelect"));var Bb=v,Vb=d,Fb=Sg,Hb=as({type:"legend.plain",newlineDisabled:!1,init:function(){this.group.add(this._contentGroup=new Fb),this._backgroundEl},getContentGroup:function(){return this._contentGroup},render:function(t,e,n){if(this.resetInner(),t.get("show",!0)){var i=t.get("align");i&&"auto"!==i||(i="right"===t.get("left")&&"vertical"===t.get("orient")?"right":"left"),this.renderInner(i,t,e,n);var r=t.getBoxLayoutParams(),o={width:n.getWidth(),height:n.getHeight()},s=t.get("padding"),l=Gr(r,o,s),u=this.layoutInner(t,i,l),h=Gr(a({width:u.width,height:u.height},r),o,s);this.group.attr("position",[h.x-u.x,h.y-u.y]),this.group.add(this._backgroundEl=tc(u,t))}},resetInner:function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl)},renderInner:function(t,e,n,i){var r=this.getContentGroup(),o=N(),a=e.get("selectedMode"),s=[];n.eachRawSeries(function(t){!t.get("legendHoverLink")&&s.push(t.id)}),Vb(e.getData(),function(l,u){var h=l.get("name");if(this.newlineDisabled||""!==h&&"\n"!==h){var c=n.getSeriesByName(h)[0];if(!o.get(h))if(c){var d=c.getData(),f=d.getVisual("color");"function"==typeof f&&(f=f(c.getDataParams(0)));var p=d.getVisual("legendSymbol")||"roundRect",g=d.getVisual("symbol");this._createItem(h,u,l,e,p,g,t,f,a).on("click",Bb(ec,h,i)).on("mouseover",Bb(nc,c,null,i,s)).on("mouseout",Bb(ic,c,null,i,s)),o.set(h,!0)}else n.eachRawSeries(function(n){if(!o.get(h)&&n.legendDataProvider){var r=n.legendDataProvider(),c=r.indexOfName(h);if(c<0)return;var d=r.getItemVisual(c,"color");this._createItem(h,u,l,e,"roundRect",null,t,d,a).on("click",Bb(ec,h,i)).on("mouseover",Bb(nc,n,h,i,s)).on("mouseout",Bb(ic,n,h,i,s)),o.set(h,!0)}},this)}else r.add(new Fb({newline:!0}))},this)},_createItem:function(t,e,n,i,r,a,s,l,u){var h=i.get("itemWidth"),c=i.get("itemHeight"),d=i.get("inactiveColor"),f=i.get("symbolKeepAspect"),p=i.isSelected(t),g=new Fb,m=n.getModel("textStyle"),v=n.get("icon"),y=n.getModel("tooltip"),x=y.parentModel;if(r=v||r,g.add(cl(r,0,0,h,c,p?l:d,null==f||f)),!v&&a&&(a!==r||"none"==a)){var _=.8*c;"none"===a&&(a="circle"),g.add(cl(a,(h-_)/2,(c-_)/2,_,_,p?l:d,null==f||f))}var w="left"===s?h+5:-5,b=s,M=i.get("formatter"),S=t;"string"==typeof M&&M?S=M.replace("{name}",null!=t?t:""):"function"==typeof M&&(S=M(t)),g.add(new kv({style:Ki({},m,{text:S,x:w,y:c/2,textFill:p?m.getTextColor():d,textAlign:b,textVerticalAlign:"middle"})}));var I=new Fv({shape:g.getBoundingRect(),invisible:!0,tooltip:y.get("show")?o({content:t,formatter:x.get("formatter",!0)||function(){return t},formatterParams:{componentType:"legend",legendIndex:i.componentIndex,name:t,$vars:["name"]}},y.option):null});return g.add(I),g.eachChild(function(t){t.silent=!0}),I.silent=!u,this.getContentGroup().add(g),qi(g),g.__legendDataIndex=e,g},layoutInner:function(t,e,n){var i=this.getContentGroup();by(t.get("orient"),i,t.get("itemGap"),n.width,n.height);var r=i.getBoundingRect();return i.attr("position",[-r.x,-r.y]),this.group.getBoundingRect()}});Ja(function(t){var e=t.findComponents({mainType:"legend"});e&&e.length&&t.filterSeries(function(t){for(var n=0;n<e.length;n++)if(!e[n].isSelected(t.name))return!1;return!0})}),Iy.registerSubTypeDefaulter("legend",function(){return"plain"});var Gb=Rb.extend({type:"legend.scroll",setScrollDataIndex:function(t){this.option.scrollDataIndex=t},defaultOption:{scrollDataIndex:0,pageButtonItemGap:5,pageButtonGap:null,pageButtonPosition:"end",pageFormatter:"{current}/{total}",pageIcons:{horizontal:["M0,0L12,-10L12,10z","M0,0L-12,-10L-12,10z"],vertical:["M0,0L20,0L10,-20z","M0,0L20,0L10,20z"]},pageIconColor:"#2f4554",pageIconInactiveColor:"#aaa",pageIconSize:15,pageTextStyle:{color:"#333"},animationDurationUpdate:800},init:function(t,e,n,i){var r=Ur(t);Gb.superCall(this,"init",t,e,n,i),rc(this,t,r)},mergeOption:function(t,e){Gb.superCall(this,"mergeOption",t,e),rc(this,this.option,t)},getOrient:function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}}}),Wb=Sg,Zb=["width","height"],Ub=["x","y"],Xb=Hb.extend({type:"legend.scroll",newlineDisabled:!0,init:function(){Xb.superCall(this,"init"),this._currentIndex=0,this.group.add(this._containerGroup=new Wb),this._containerGroup.add(this.getContentGroup()),this.group.add(this._controllerGroup=new Wb),this._showController},resetInner:function(){Xb.superCall(this,"resetInner"),this._controllerGroup.removeAll(),this._containerGroup.removeClipPath(),this._containerGroup.__rectSize=null},renderInner:function(t,e,n,i){function r(t,n){var r=t+"DataIndex",l=fr(e.get("pageIcons",!0)[e.getOrient().name][n],{onclick:m(o._pageGo,o,r,e,i)},{x:-s[0]/2,y:-s[1]/2,width:s[0],height:s[1]});l.name=t,a.add(l)}var o=this;Xb.superCall(this,"renderInner",t,e,n,i);var a=this._controllerGroup,s=e.get("pageIconSize",!0);y(s)||(s=[s,s]),r("pagePrev",0);var l=e.getModel("pageTextStyle");a.add(new kv({name:"pageText",style:{textFill:l.getTextColor(),font:l.getFont(),textVerticalAlign:"middle",textAlign:"center"},silent:!0})),r("pageNext",1)},layoutInner:function(t,e,n){var i=this.getContentGroup(),r=this._containerGroup,o=this._controllerGroup,a=t.getOrient().index,s=Zb[a],l=Zb[1-a],u=Ub[1-a];by(t.get("orient"),i,t.get("itemGap"),a?n.width:null,a?null:n.height),by("horizontal",o,t.get("pageButtonItemGap",!0));var h=i.getBoundingRect(),c=o.getBoundingRect(),d=this._showController=h[s]>n[s],f=[-h.x,-h.y];f[a]=i.position[a];var p=[0,0],g=[-c.x,-c.y],m=T(t.get("pageButtonGap",!0),t.get("itemGap",!0));d&&("end"===t.get("pageButtonPosition",!0)?g[a]+=n[s]-c[s]:p[a]+=c[s]+m),g[1-a]+=h[l]/2-c[l]/2,i.attr("position",f),r.attr("position",p),o.attr("position",g);var v=this.group.getBoundingRect();if((v={x:0,y:0})[s]=d?n[s]:h[s],v[l]=Math.max(h[l],c[l]),v[u]=Math.min(0,c[u]+g[1-a]),r.__rectSize=n[s],d){var y={x:0,y:0};y[s]=Math.max(n[s]-c[s]-m,0),y[l]=v[l],r.setClipPath(new Fv({shape:y})),r.__rectSize=y[s]}else o.eachChild(function(t){t.attr({invisible:!0,silent:!0})});var x=this._getPageInfo(t);return null!=x.pageIndex&&ar(i,{position:x.contentPosition},!!d&&t),this._updatePageInfoView(t,x),v},_pageGo:function(t,e,n){var i=this._getPageInfo(e)[t];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:e.id})},_updatePageInfoView:function(t,e){var n=this._controllerGroup;d(["pagePrev","pageNext"],function(i){var r=null!=e[i+"DataIndex"],o=n.childOfName(i);o&&(o.setStyle("fill",r?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),o.cursor=r?"pointer":"default")});var i=n.childOfName("pageText"),r=t.get("pageFormatter"),o=e.pageIndex,a=null!=o?o+1:0,s=e.pageCount;i&&r&&i.setStyle("text",_(r)?r.replace("{current}",a).replace("{total}",s):r({current:a,total:s}))},_getPageInfo:function(t){function e(t){var e=t.getBoundingRect().clone();return e[f]+=t.position[h],e}var n,i,r,o,a=t.get("scrollDataIndex",!0),s=this.getContentGroup(),l=s.getBoundingRect(),u=this._containerGroup.__rectSize,h=t.getOrient().index,c=Zb[h],d=Zb[1-h],f=Ub[h],p=s.position.slice();this._showController?s.eachChild(function(t){t.__legendDataIndex===a&&(o=t)}):o=s.childAt(0);var g=u?Math.ceil(l[c]/u):0;if(o){var m=o.getBoundingRect(),v=o.position[h]+m[f];p[h]=-v-l[f],n=Math.floor(g*(v+m[f]+u/2)/l[c]),n=l[c]&&g?Math.max(0,Math.min(g-1,n)):-1;var y={x:0,y:0};y[c]=u,y[d]=l[d],y[f]=-p[h]-l[f];var x,_=s.children();if(s.eachChild(function(t,n){var i=e(t);i.intersect(y)&&(null==x&&(x=n),r=t.__legendDataIndex),n===_.length-1&&i[f]+i[c]<=y[f]+y[c]&&(r=null)}),null!=x){var w=e(_[x]);if(y[f]=w[f]+w[c]-y[c],x<=0&&w[f]>=y[f])i=null;else{for(;x>0&&e(_[x-1]).intersect(y);)x--;i=_[x].__legendDataIndex}}}return{contentPosition:p,pageIndex:n,pageCount:g,pagePrevDataIndex:i,pageNextDataIndex:r}}});ts("legendScroll","legendscroll",function(t,e){var n=t.scrollDataIndex;null!=n&&e.eachComponent({mainType:"legend",subType:"scroll",query:t},function(t){t.setScrollDataIndex(n)})}),os({type:"title",layoutMode:{type:"box",ignoreSize:!0},defaultOption:{zlevel:0,z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bolder",color:"#333"},subtextStyle:{color:"#aaa"}}}),as({type:"title",render:function(t,e,n){if(this.group.removeAll(),t.get("show")){var i=this.group,r=t.getModel("textStyle"),o=t.getModel("subtextStyle"),a=t.get("textAlign"),s=t.get("textBaseline"),l=new kv({style:Ki({},r,{text:t.get("text"),textFill:r.getTextColor()},{disableBox:!0}),z2:10}),u=l.getBoundingRect(),h=t.get("subtext"),c=new kv({style:Ki({},o,{text:h,textFill:o.getTextColor(),y:u.height+t.get("itemGap"),textVerticalAlign:"top"},{disableBox:!0}),z2:10}),d=t.get("link"),f=t.get("sublink");l.silent=!d,c.silent=!f,d&&l.on("click",function(){window.open(d,"_"+t.get("target"))}),f&&c.on("click",function(){window.open(f,"_"+t.get("subtarget"))}),i.add(l),h&&i.add(c);var p=i.getBoundingRect(),g=t.getBoxLayoutParams();g.width=p.width,g.height=p.height;var m=Gr(g,{width:n.getWidth(),height:n.getHeight()},t.get("padding"));a||("middle"===(a=t.get("left")||t.get("right"))&&(a="center"),"right"===a?m.x+=m.width:"center"===a&&(m.x+=m.width/2)),s||("center"===(s=t.get("top")||t.get("bottom"))&&(s="middle"),"bottom"===s?m.y+=m.height:"middle"===s&&(m.y+=m.height/2),s=s||"top"),i.attr("position",[m.x,m.y]);var v={textAlign:a,textVerticalAlign:s};l.setStyle(v),c.setStyle(v),p=i.getBoundingRect();var y=m.margin,x=t.getItemStyle(["color","opacity"]);x.fill=t.get("backgroundColor");var _=new Fv({shape:{x:p.x-y[3],y:p.y-y[0],width:p.width+y[1]+y[3],height:p.height+y[0]+y[2],r:t.get("borderRadius")},style:x,silent:!0});Ei(_),i.add(_)}}});var jb=Or,Yb=Er,qb=os({type:"marker",dependencies:["series","grid","polar","geo"],init:function(t,e,n,i){this.mergeDefaultAndTheme(t,n),this.mergeOption(t,n,i.createdBySelf,!0)},isAnimationEnabled:function(){if(bp.node)return!1;var t=this.__hostSeries;return this.getShallow("animation")&&t&&t.isAnimationEnabled()},mergeOption:function(t,e,n,i){var r=this.constructor,a=this.mainType+"Model";n||e.eachSeries(function(t){var n=t.get(this.mainType,!0),s=t[a];n&&n.data?(s?s.mergeOption(n,e,!0):(i&&oc(n),d(n.data,function(t){t instanceof Array?(oc(t[0]),oc(t[1])):oc(t)}),o(s=new r(n,this,e),{mainType:this.mainType,seriesIndex:t.seriesIndex,name:t.name,createdBySelf:!0}),s.__hostSeries=t),t[a]=s):t[a]=null},this)},formatTooltip:function(t){var e=this.getData(),n=this.getRawValue(t),i=y(n)?f(n,jb).join(", "):jb(n),r=e.getName(t),o=Yb(this.name);return(null!=n||r)&&(o+="<br />"),r&&(o+=Yb(r),null!=n&&(o+=" : ")),null!=n&&(o+=Yb(i)),o},getData:function(){return this._data},setData:function(t){this._data=t}});h(qb,sx),qb.extend({type:"markPoint",defaultOption:{zlevel:0,z:5,symbol:"pin",symbolSize:50,tooltip:{trigger:"item"},label:{show:!0,position:"inside"},itemStyle:{borderWidth:2},emphasis:{label:{show:!0}}}});var $b=l,Kb=v,Qb={min:Kb(lc,"min"),max:Kb(lc,"max"),average:Kb(lc,"average")},Jb=as({type:"marker",init:function(){this.markerGroupMap=N()},render:function(t,e,n){var i=this.markerGroupMap;i.each(function(t){t.__keep=!1});var r=this.type+"Model";e.eachSeries(function(t){var i=t[r];i&&this.renderSeries(t,i,e,n)},this),i.each(function(t){!t.__keep&&this.group.remove(t.group)},this)},renderSeries:function(){}});Jb.extend({type:"markPoint",updateTransform:function(t,e,n){e.eachSeries(function(t){var e=t.markPointModel;e&&(gc(e.getData(),t,n),this.markerGroupMap.get(t.id).updateLayout(e))},this)},renderSeries:function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new Bl),u=mc(r,t,e);e.setData(u),gc(e.getData(),t,i),u.each(function(t){var n=u.getItemModel(t),i=n.getShallow("symbolSize");"function"==typeof i&&(i=i(e.getRawValue(t),e.getDataParams(t))),u.setItemVisual(t,{symbolSize:i,color:n.get("itemStyle.color")||a.getVisual("color"),symbol:n.getShallow("symbol")})}),l.updateData(u),this.group.add(l.group),u.eachItemGraphicEl(function(t){t.traverse(function(t){t.dataModel=e})}),l.__keep=!0,l.group.silent=e.get("silent")||t.get("silent")}}),Qa(function(t){t.markPoint=t.markPoint||{}}),qb.extend({type:"markLine",defaultOption:{zlevel:0,z:5,symbol:["circle","arrow"],symbolSize:[8,16],precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end"},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"}});var tM=Hv.prototype,eM=Wv.prototype,nM=Ai({type:"ec-line",style:{stroke:"#000",fill:null},shape:{x1:0,y1:0,x2:0,y2:0,percent:1,cpx1:null,cpy1:null},buildPath:function(t,e){(vc(e)?tM:eM).buildPath(t,e)},pointAt:function(t){return vc(this.shape)?tM.pointAt.call(this,t):eM.pointAt.call(this,t)},tangentAt:function(t){var e=this.shape,n=vc(e)?[e.x2-e.x1,e.y2-e.y1]:eM.tangentAt.call(this,t);return j(n,n)}}),iM=["fromSymbol","toSymbol"],rM=bc.prototype;rM.beforeUpdate=function(){var t=this,e=t.childOfName("fromSymbol"),n=t.childOfName("toSymbol"),i=t.childOfName("label");if(e||n||!i.ignore){for(var r=1,o=this.parent;o;)o.scale&&(r/=o.scale[0]),o=o.parent;var a=t.childOfName("line");if(this.__dirty||a.__dirty){var s=a.shape.percent,l=a.pointAt(0),u=a.pointAt(s),h=W([],u,l);if(j(h,h),e&&(e.attr("position",l),c=a.tangentAt(0),e.attr("rotation",Math.PI/2-Math.atan2(c[1],c[0])),e.attr("scale",[r*s,r*s])),n){n.attr("position",u);var c=a.tangentAt(1);n.attr("rotation",-Math.PI/2-Math.atan2(c[1],c[0])),n.attr("scale",[r*s,r*s])}if(!i.ignore){i.attr("position",u);var d,f,p,g=5*r;if("end"===i.__position)d=[h[0]*g+u[0],h[1]*g+u[1]],f=h[0]>.8?"left":h[0]<-.8?"right":"center",p=h[1]>.8?"top":h[1]<-.8?"bottom":"middle";else if("middle"===i.__position){var m=s/2,v=[(c=a.tangentAt(m))[1],-c[0]],y=a.pointAt(m);v[1]>0&&(v[0]=-v[0],v[1]=-v[1]),d=[y[0]+v[0]*g,y[1]+v[1]*g],f="center",p="bottom";var x=-Math.atan2(c[1],c[0]);u[0]<l[0]&&(x=Math.PI+x),i.attr("rotation",x)}else d=[-h[0]*g+l[0],-h[1]*g+l[1]],f=h[0]>.8?"right":h[0]<-.8?"left":"center",p=h[1]>.8?"bottom":h[1]<-.8?"top":"middle";i.attr({style:{textVerticalAlign:i.__verticalAlign||p,textAlign:i.__textAlign||f},position:d,scale:[r,r]})}}}},rM._createLine=function(t,e,n){var i=t.hostModel,r=_c(t.getItemLayout(e));r.shape.percent=0,sr(r,{shape:{percent:1}},i,e),this.add(r);var o=new kv({name:"label"});this.add(o),d(iM,function(n){var i=xc(n,t,e);this.add(i),this[yc(n)]=t.getItemVisual(e,n)},this),this._updateCommonStl(t,e,n)},rM.updateData=function(t,e,n){var i=t.hostModel,r=this.childOfName("line"),o=t.getItemLayout(e),a={shape:{}};wc(a.shape,o),ar(r,a,i,e),d(iM,function(n){var i=t.getItemVisual(e,n),r=yc(n);if(this[r]!==i){this.remove(this.childOfName(n));var o=xc(n,t,e);this.add(o)}this[r]=i},this),this._updateCommonStl(t,e,n)},rM._updateCommonStl=function(t,e,n){var i=t.hostModel,r=this.childOfName("line"),o=n&&n.lineStyle,s=n&&n.hoverLineStyle,l=n&&n.labelModel,u=n&&n.hoverLabelModel;if(!n||t.hasItemOption){var h=t.getItemModel(e);o=h.getModel("lineStyle").getLineStyle(),s=h.getModel("emphasis.lineStyle").getLineStyle(),l=h.getModel("label"),u=h.getModel("emphasis.label")}var c=t.getItemVisual(e,"color"),f=D(t.getItemVisual(e,"opacity"),o.opacity,1);r.useStyle(a({strokeNoScale:!0,fill:"none",stroke:c,opacity:f},o)),r.hoverStyle=s,d(iM,function(t){var e=this.childOfName(t);e&&(e.setColor(c),e.setStyle({opacity:f}))},this);var p,g,m=l.getShallow("show"),v=u.getShallow("show"),y=this.childOfName("label");if((m||v)&&(p=c||"#000",null==(g=i.getFormattedLabel(e,"normal",t.dataType)))){var x=i.getRawValue(e);g=null==x?t.getName(e):isFinite(x)?wr(x):x}var _=m?g:null,w=v?T(i.getFormattedLabel(e,"emphasis",t.dataType),g):null,b=y.style;null==_&&null==w||(Ki(y.style,l,{text:_},{autoColor:p}),y.__textAlign=b.textAlign,y.__verticalAlign=b.textVerticalAlign,y.__position=l.get("position")||"middle"),y.hoverStyle=null!=w?{text:w,textFill:u.getTextColor(!0),fontStyle:u.getShallow("fontStyle"),fontWeight:u.getShallow("fontWeight"),fontSize:u.getShallow("fontSize"),fontFamily:u.getShallow("fontFamily")}:{text:null},y.ignore=!m&&!v,qi(this)},rM.highlight=function(){this.trigger("emphasis")},rM.downplay=function(){this.trigger("normal")},rM.updateLayout=function(t,e){this.setLinePoints(t.getItemLayout(e))},rM.setLinePoints=function(t){var e=this.childOfName("line");wc(e.shape,t),e.dirty()},u(bc,Sg);var oM=Mc.prototype;oM.isPersistent=function(){return!0},oM.updateData=function(t){var e=this,n=e.group,i=e._lineData;e._lineData=t,i||n.removeAll();var r=Cc(t);t.diff(i).add(function(n){Sc(e,t,n,r)}).update(function(n,o){Ic(e,i,t,o,n,r)}).remove(function(t){n.remove(i.getItemGraphicEl(t))}).execute()},oM.updateLayout=function(){var t=this._lineData;t&&t.eachItemGraphicEl(function(e,n){e.updateLayout(t,n)},this)},oM.incrementalPrepareUpdate=function(t){this._seriesScope=Cc(t),this._lineData=null,this.group.removeAll()},oM.incrementalUpdate=function(t,e){for(var n=t.start;n<t.end;n++)if(Dc(e.getItemLayout(n))){var i=new this._ctor(e,n,this._seriesScope);i.traverse(function(t){t.isGroup||(t.incremental=t.useHoverLayer=!0)}),this.group.add(i),e.setItemGraphicEl(n,i)}},oM.remove=function(){this._clearIncremental(),this._incremental=null,this.group.removeAll()},oM._clearIncremental=function(){var t=this._incremental;t&&t.clearDisplaybles()};var aM=function(t,e,r,a){var s=t.getData(),l=a.type;if(!y(a)&&("min"===l||"max"===l||"average"===l||"median"===l||null!=a.xAxis||null!=a.yAxis)){var u,h;if(null!=a.yAxis||null!=a.xAxis)u=null!=a.yAxis?"y":"x",e.getAxis(u),h=C(a.yAxis,a.xAxis);else{var c=hc(a,s,e,t);u=c.valueDataDim,c.valueAxis,h=pc(s,u,l)}var d="x"===u?0:1,f=1-d,p=n(a),g={};p.type=null,p.coord=[],g.coord=[],p.coord[f]=-1/0,g.coord[f]=1/0;var m=r.get("precision");m>=0&&"number"==typeof h&&(h=+h.toFixed(Math.min(m,20))),p.coord[d]=g.coord[d]=h,a=[p,g,{type:l,valueIndex:a.valueIndex,value:h}]}return a=[uc(t,a[0]),uc(t,a[1]),o({},a[2])],a[2].type=a[2].type||"",i(a[2],a[0]),i(a[2],a[1]),a};Jb.extend({type:"markLine",updateTransform:function(t,e,n){e.eachSeries(function(t){var e=t.markLineModel;if(e){var i=e.getData(),r=e.__from,o=e.__to;r.each(function(e){Lc(r,e,!0,t,n),Lc(o,e,!1,t,n)}),i.each(function(t){i.setItemLayout(t,[r.getItemLayout(t),o.getItemLayout(t)])}),this.markerGroupMap.get(t.id).updateLayout()}},this)},renderSeries:function(t,e,n,i){function r(e,n,r){var o=e.getItemModel(n);Lc(e,n,r,t,i),e.setItemVisual(n,{symbolSize:o.get("symbolSize")||g[r?0:1],symbol:o.get("symbol",!0)||p[r?0:1],color:o.get("itemStyle.color")||s.getVisual("color")})}var o=t.coordinateSystem,a=t.id,s=t.getData(),l=this.markerGroupMap,u=l.get(a)||l.set(a,new Mc);this.group.add(u.group);var h=Oc(o,t,e),c=h.from,d=h.to,f=h.line;e.__from=c,e.__to=d,e.setData(f);var p=e.get("symbol"),g=e.get("symbolSize");y(p)||(p=[p,p]),"number"==typeof g&&(g=[g,g]),h.from.each(function(t){r(c,t,!0),r(d,t,!1)}),f.each(function(t){var e=f.getItemModel(t).get("lineStyle.color");f.setItemVisual(t,{color:e||c.getItemVisual(t,"color")}),f.setItemLayout(t,[c.getItemLayout(t),d.getItemLayout(t)]),f.setItemVisual(t,{fromSymbolSize:c.getItemVisual(t,"symbolSize"),fromSymbol:c.getItemVisual(t,"symbol"),toSymbolSize:d.getItemVisual(t,"symbolSize"),toSymbol:d.getItemVisual(t,"symbol")})}),u.updateData(f),h.line.eachItemGraphicEl(function(t,n){t.traverse(function(t){t.dataModel=e})}),u.__keep=!0,u.group.silent=e.get("silent")||t.get("silent")}}),Qa(function(t){t.markLine=t.markLine||{}}),qb.extend({type:"markArea",defaultOption:{zlevel:0,z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}}});var sM=function(t,e,n,i){var o=uc(t,i[0]),a=uc(t,i[1]),s=C,l=o.coord,u=a.coord;l[0]=s(l[0],-1/0),l[1]=s(l[1],-1/0),u[0]=s(u[0],1/0),u[1]=s(u[1],1/0);var h=r([{},o,a]);return h.coord=[o.coord,a.coord],h.x0=o.x,h.y0=o.y,h.x1=a.x,h.y1=a.y,h},lM=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]];Jb.extend({type:"markArea",updateTransform:function(t,e,n){e.eachSeries(function(t){var e=t.markAreaModel;if(e){var i=e.getData();i.each(function(e){var r=f(lM,function(r){return Rc(i,e,r,t,n)});i.setItemLayout(e,r),i.getItemGraphicEl(e).setShape("points",r)})}},this)},renderSeries:function(t,e,n,i){var r=t.coordinateSystem,o=t.id,s=t.getData(),l=this.markerGroupMap,u=l.get(o)||l.set(o,{group:new Sg});this.group.add(u.group),u.__keep=!0;var h=Bc(r,t,e);e.setData(h),h.each(function(e){h.setItemLayout(e,f(lM,function(n){return Rc(h,e,n,t,i)})),h.setItemVisual(e,{color:s.getVisual("color")})}),h.diff(u.__data).add(function(t){var e=new Bv({shape:{points:h.getItemLayout(t)}});h.setItemGraphicEl(t,e),u.group.add(e)}).update(function(t,n){var i=u.__data.getItemGraphicEl(n);ar(i,{shape:{points:h.getItemLayout(t)}},e,t),u.group.add(i),h.setItemGraphicEl(t,i)}).remove(function(t){var e=u.__data.getItemGraphicEl(t);u.group.remove(e)}).execute(),h.eachItemGraphicEl(function(t,n){var i=h.getItemModel(n),r=i.getModel("label"),o=i.getModel("emphasis.label"),s=h.getItemVisual(n,"color");t.useStyle(a(i.getModel("itemStyle").getItemStyle(),{fill:Pt(s,.4),stroke:s})),t.hoverStyle=i.getModel("emphasis.itemStyle").getItemStyle(),$i(t.style,t.hoverStyle,r,o,{labelFetcher:e,labelDataIndex:n,defaultText:h.getName(n)||"",isRectText:!0,autoColor:s}),qi(t,{}),t.dataModel=e}),u.__data=h,u.group.silent=e.get("silent")||t.get("silent")}}),Qa(function(t){t.markArea=t.markArea||{}}),Iy.registerSubTypeDefaulter("dataZoom",function(){return"slider"});var uM=["cartesian2d","polar","singleAxis"],hM=function(t,e){var n=f(t=t.slice(),Fr),i=f(e=(e||[]).slice(),Fr);return function(r,o){d(t,function(t,a){for(var s={name:t,capital:n[a]},l=0;l<e.length;l++)s[e[l]]=t+i[l];r.call(o,s)})}}(["x","y","z","radius","angle","single"],["axisIndex","axis","index","id"]),cM=d,dM=br,fM=function(t,e,n,i){this._dimName=t,this._axisIndex=e,this._valueWindow,this._percentWindow,this._dataExtent,this._minMaxSpan,this.ecModel=i,this._dataZoomModel=n};fM.prototype={constructor:fM,hostedBy:function(t){return this._dataZoomModel===t},getDataValueWindow:function(){return this._valueWindow.slice()},getDataPercentWindow:function(){return this._percentWindow.slice()},getTargetSeriesModels:function(){var t=[],e=this.ecModel;return e.eachSeries(function(n){if(Vc(n.get("coordinateSystem"))){var i=this._dimName,r=e.queryComponents({mainType:i+"Axis",index:n.get(i+"AxisIndex"),id:n.get(i+"AxisId")})[0];this._axisIndex===(r&&r.componentIndex)&&t.push(n)}},this),t},getAxisModel:function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},getOtherAxisModel:function(){var t,e,n=this._dimName,i=this.ecModel,r=this.getAxisModel();"x"===n||"y"===n?(e="gridIndex",t="x"===n?"y":"x"):(e="polarIndex",t="angle"===n?"radius":"angle");var o;return i.eachComponent(t+"Axis",function(t){(t.get(e)||0)===(r.get(e)||0)&&(o=t)}),o},getMinMaxSpan:function(){return n(this._minMaxSpan)},calculateDataWindow:function(t){var e=this._dataExtent,n=this.getAxisModel().axis.scale,i=this._dataZoomModel.getRangePropMode(),r=[0,100],o=[t.start,t.end],a=[];return cM(["startValue","endValue"],function(e){a.push(null!=t[e]?n.parse(t[e]):null)}),cM([0,1],function(t){var s=a[t],l=o[t];"percent"===i[t]?(null==l&&(l=r[t]),s=n.parse(xr(l,r,e,!0))):l=xr(s,e,r,!0),a[t]=s,o[t]=l}),{valueWindow:dM(a),percentWindow:dM(o)}},reset:function(t){if(t===this._dataZoomModel){var e=this.getTargetSeriesModels();this._dataExtent=Hc(this,this._dimName,e);var n=this.calculateDataWindow(t.option);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,Zc(this),Wc(this)}},restore:function(t){t===this._dataZoomModel&&(this._valueWindow=this._percentWindow=null,Wc(this,!0))},filterData:function(t,e){function n(t){return t>=a[0]&&t<=a[1]}if(t===this._dataZoomModel){var i=this._dimName,r=this.getTargetSeriesModels(),o=t.get("filterMode"),a=this._valueWindow;"none"!==o&&cM(r,function(t){var e=t.getData(),r=e.mapDimension(i,!0);"weakFilter"===o?e.filterSelf(function(t){for(var n,i,o,s=0;s<r.length;s++){var l=e.get(r[s],t),u=!isNaN(l),h=l<a[0],c=l>a[1];if(u&&!h&&!c)return!0;u&&(o=!0),h&&(n=!0),c&&(i=!0)}return o&&n&&i}):cM(r,function(i){if("empty"===o)t.setData(e.map(i,function(t){return n(t)?t:NaN}));else{var r={};r[i]=a,e.selectRange(r)}}),cM(r,function(t){e.setApproximateExtent(a,t)})})}}};var pM=d,gM=hM,mM=os({type:"dataZoom",dependencies:["xAxis","yAxis","zAxis","radiusAxis","angleAxis","singleAxis","series"],defaultOption:{zlevel:0,z:4,orient:null,xAxisIndex:null,yAxisIndex:null,filterMode:"filter",throttle:null,start:0,end:100,startValue:null,endValue:null,minSpan:null,maxSpan:null,minValueSpan:null,maxValueSpan:null,rangeMode:null},init:function(t,e,n){this._dataIntervalByAxis={},this._dataInfo={},this._axisProxies={},this.textStyleModel,this._autoThrottle=!0,this._rangePropMode=["percent","percent"];var i=Uc(t);this.mergeDefaultAndTheme(t,n),this.doInit(i)},mergeOption:function(t){var e=Uc(t);i(this.option,t,!0),this.doInit(e)},doInit:function(t){var e=this.option;bp.canvasSupported||(e.realtime=!1),this._setDefaultThrottle(t),Xc(this,t),pM([["start","startValue"],["end","endValue"]],function(t,n){"value"===this._rangePropMode[n]&&(e[t[0]]=null)},this),this.textStyleModel=this.getModel("textStyle"),this._resetTarget(),this._giveAxisProxies()},_giveAxisProxies:function(){var t=this._axisProxies;this.eachTargetAxis(function(e,n,i,r){var o=this.dependentModels[e.axis][n],a=o.__dzAxisProxy||(o.__dzAxisProxy=new fM(e.name,n,this,r));t[e.name+"_"+n]=a},this)},_resetTarget:function(){var t=this.option,e=this._judgeAutoMode();gM(function(e){var n=e.axisIndex;t[n]=xn(t[n])},this),"axisIndex"===e?this._autoSetAxisIndex():"orient"===e&&this._autoSetOrient()},_judgeAutoMode:function(){var t=this.option,e=!1;gM(function(n){null!=t[n.axisIndex]&&(e=!0)},this);var n=t.orient;return null==n&&e?"orient":e?void 0:(null==n&&(t.orient="horizontal"),"axisIndex")},_autoSetAxisIndex:function(){var t=!0,e=this.get("orient",!0),n=this.option,i=this.dependentModels;if(t){var r="vertical"===e?"y":"x";i[r+"Axis"].length?(n[r+"AxisIndex"]=[0],t=!1):pM(i.singleAxis,function(i){t&&i.get("orient",!0)===e&&(n.singleAxisIndex=[i.componentIndex],t=!1)})}t&&gM(function(e){if(t){var i=[],r=this.dependentModels[e.axis];if(r.length&&!i.length)for(var o=0,a=r.length;o<a;o++)"category"===r[o].get("type")&&i.push(o);n[e.axisIndex]=i,i.length&&(t=!1)}},this),t&&this.ecModel.eachSeries(function(t){this._isSeriesHasAllAxesTypeOf(t,"value")&&gM(function(e){var i=n[e.axisIndex],r=t.get(e.axisIndex),o=t.get(e.axisId);l(i,r=t.ecModel.queryComponents({mainType:e.axis,index:r,id:o})[0].componentIndex)<0&&i.push(r)})},this)},_autoSetOrient:function(){var t;this.eachTargetAxis(function(e){!t&&(t=e.name)},this),this.option.orient="y"===t?"vertical":"horizontal"},_isSeriesHasAllAxesTypeOf:function(t,e){var n=!0;return gM(function(i){var r=t.get(i.axisIndex),o=this.dependentModels[i.axis][r];o&&o.get("type")===e||(n=!1)},this),n},_setDefaultThrottle:function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var e=this.ecModel.option;this.option.throttle=e.animation&&e.animationDurationUpdate>0?100:20}},getFirstTargetAxisModel:function(){var t;return gM(function(e){if(null==t){var n=this.get(e.axisIndex);n.length&&(t=this.dependentModels[e.axis][n[0]])}},this),t},eachTargetAxis:function(t,e){var n=this.ecModel;gM(function(i){pM(this.get(i.axisIndex),function(r){t.call(e,i,r,this,n)},this)},this)},getAxisProxy:function(t,e){return this._axisProxies[t+"_"+e]},getAxisModel:function(t,e){var n=this.getAxisProxy(t,e);return n&&n.getAxisModel()},setRawRange:function(t,e){var n=this.option;pM([["start","startValue"],["end","endValue"]],function(e){null==t[e[0]]&&null==t[e[1]]||(n[e[0]]=t[e[0]],n[e[1]]=t[e[1]])},this),!e&&Xc(this,t)},getPercentRange:function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getDataPercentWindow()},getValueRange:function(t,e){if(null!=t||null!=e)return this.getAxisProxy(t,e).getDataValueWindow();var n=this.findRepresentativeAxisProxy();return n?n.getDataValueWindow():void 0},findRepresentativeAxisProxy:function(t){if(t)return t.__dzAxisProxy;var e=this._axisProxies;for(var n in e)if(e.hasOwnProperty(n)&&e[n].hostedBy(this))return e[n];for(var n in e)if(e.hasOwnProperty(n)&&!e[n].hostedBy(this))return e[n]},getRangePropMode:function(){return this._rangePropMode.slice()}}),vM=dx.extend({type:"dataZoom",render:function(t,e,n,i){this.dataZoomModel=t,this.ecModel=e,this.api=n},getTargetCoordInfo:function(){function t(t,e,n,i){for(var r,o=0;o<n.length;o++)if(n[o].model===t){r=n[o];break}r||n.push(r={model:t,axisModels:[],coordIndex:i}),r.axisModels.push(e)}var e=this.dataZoomModel,n=this.ecModel,i={};return e.eachTargetAxis(function(e,r){var o=n.getComponent(e.axis,r);if(o){var a=o.getCoordSysModel();a&&t(a,o,i[a.mainType]||(i[a.mainType]=[]),a.componentIndex)}},this),i}}),yM=(mM.extend({type:"dataZoom.slider",layoutMode:"box",defaultOption:{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#2f4554",width:.5,opacity:.3},areaStyle:{color:"rgba(47,69,84,0.3)",opacity:.3}},borderColor:"#ddd",fillerColor:"rgba(167,183,204,0.4)",handleIcon:"M8.2,13.6V3.9H6.3v9.7H3.1v14.9h3.3v9.7h1.8v-9.7h3.3V13.6H8.2z M9.7,24.4H4.8v-1.4h4.9V24.4z M9.7,19.1H4.8v-1.4h4.9V19.1z",handleSize:"100%",handleStyle:{color:"#a7b7cc"},labelPrecision:null,labelFormatter:null,showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#333"}}}),function(t,e,n,i,r,o){e[0]=Yc(e[0],n),e[1]=Yc(e[1],n),t=t||0;var a=n[1]-n[0];null!=r&&(r=Yc(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i&&(r=o=Math.abs(e[1]-e[0]),i=0);var s=jc(e,i);e[i]+=t;var l=r||0,u=n.slice();s.sign<0?u[0]+=l:u[1]-=l,e[i]=Yc(e[i],u);h=jc(e,i);null!=r&&(h.sign!==s.sign||h.span<r)&&(e[1-i]=e[i]+s.sign*r);var h=jc(e,i);return null!=o&&h.span>o&&(e[1-i]=e[i]+h.sign*o),e}),xM=Fv,_M=xr,wM=br,bM=m,MM=d,SM="horizontal",IM=5,CM=["line","bar","candlestick","scatter"],TM=vM.extend({type:"dataZoom.slider",init:function(t,e){this._displayables={},this._orient,this._range,this._handleEnds,this._size,this._handleWidth,this._handleHeight,this._location,this._dragging,this._dataShadowInfo,this.api=e},render:function(t,e,n,i){TM.superApply(this,"render",arguments),ha(this,"_dispatchZoomAction",this.dataZoomModel.get("throttle"),"fixRate"),this._orient=t.get("orient"),!1!==this.dataZoomModel.get("show")?(i&&"dataZoom"===i.type&&i.from===this.uid||this._buildView(),this._updateView()):this.group.removeAll()},remove:function(){TM.superApply(this,"remove",arguments),ca(this,"_dispatchZoomAction")},dispose:function(){TM.superApply(this,"dispose",arguments),ca(this,"_dispatchZoomAction")},_buildView:function(){var t=this.group;t.removeAll(),this._resetLocation(),this._resetInterval();var e=this._displayables.barGroup=new Sg;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(e),this._positionGroup()},_resetLocation:function(){var t=this.dataZoomModel,e=this.api,n=this._findCoordRect(),i={width:e.getWidth(),height:e.getHeight()},r=this._orient===SM?{right:i.width-n.x-n.width,top:i.height-30-7,width:n.width,height:30}:{right:7,top:n.y,width:30,height:n.height},o=Ur(t.option);d(["right","top","width","height"],function(t){"ph"===o[t]&&(o[t]=r[t])});var a=Gr(o,i,t.padding);this._location={x:a.x,y:a.y},this._size=[a.width,a.height],"vertical"===this._orient&&this._size.reverse()},_positionGroup:function(){var t=this.group,e=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),r=i&&i.get("inverse"),o=this._displayables.barGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;o.attr(n!==SM||r?n===SM&&r?{scale:a?[-1,1]:[-1,-1]}:"vertical"!==n||r?{scale:a?[-1,-1]:[-1,1],rotation:Math.PI/2}:{scale:a?[1,-1]:[1,1],rotation:Math.PI/2}:{scale:a?[1,1]:[1,-1]});var s=t.getBoundingRect([o]);t.attr("position",[e.x-s.x,e.y-s.y])},_getViewExtent:function(){return[0,this._size[0]]},_renderBackground:function(){var t=this.dataZoomModel,e=this._size,n=this._displayables.barGroup;n.add(new xM({silent:!0,shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:t.get("backgroundColor")},z2:-40})),n.add(new xM({shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:"transparent"},z2:0,onclick:m(this._onClickPanelClick,this)}))},_renderDataShadow:function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(t){var e=this._size,n=t.series,i=n.getRawData(),r=n.getShadowDim?n.getShadowDim():t.otherDim;if(null!=r){var o=i.getDataExtent(r),s=.3*(o[1]-o[0]);o=[o[0]-s,o[1]+s];var l,u=[0,e[1]],h=[0,e[0]],c=[[e[0],0],[0,0]],d=[],f=h[1]/(i.count()-1),p=0,g=Math.round(i.count()/e[0]);i.each([r],function(t,e){if(g>0&&e%g)p+=f;else{var n=null==t||isNaN(t)||""===t,i=n?0:_M(t,o,u,!0);n&&!l&&e?(c.push([c[c.length-1][0],0]),d.push([d[d.length-1][0],0])):!n&&l&&(c.push([p,0]),d.push([p,0])),c.push([p,i]),d.push([p,i]),p+=f,l=n}});var m=this.dataZoomModel;this._displayables.barGroup.add(new Bv({shape:{points:c},style:a({fill:m.get("dataBackgroundColor")},m.getModel("dataBackground.areaStyle").getAreaStyle()),silent:!0,z2:-20})),this._displayables.barGroup.add(new Vv({shape:{points:d},style:m.getModel("dataBackground.lineStyle").getLineStyle(),silent:!0,z2:-19}))}}},_prepareDataShadowInfo:function(){var t=this.dataZoomModel,e=t.get("showDataShadow");if(!1!==e){var n,i=this.ecModel;return t.eachTargetAxis(function(r,o){d(t.getAxisProxy(r.name,o).getTargetSeriesModels(),function(t){if(!(n||!0!==e&&l(CM,t.get("type"))<0)){var a,s=i.getComponent(r.axis,o).axis,u=qc(r.name),h=t.coordinateSystem;null!=u&&h.getOtherAxis&&(a=h.getOtherAxis(s).inverse),u=t.getData().mapDimension(u),n={thisAxis:s,series:t,thisDim:r.name,otherDim:u,otherAxisInverse:a}}},this)},this),n}},_renderHandle:function(){var t=this._displayables,e=t.handles=[],n=t.handleLabels=[],i=this._displayables.barGroup,r=this._size,o=this.dataZoomModel;i.add(t.filler=new xM({draggable:!0,cursor:$c(this._orient),drift:bM(this._onDragMove,this,"all"),onmousemove:function(t){tm(t.event)},ondragstart:bM(this._showDataInfo,this,!0),ondragend:bM(this._onDragEnd,this),onmouseover:bM(this._showDataInfo,this,!0),onmouseout:bM(this._showDataInfo,this,!1),style:{fill:o.get("fillerColor"),textPosition:"inside"}})),i.add(new xM(Ei({silent:!0,shape:{x:0,y:0,width:r[0],height:r[1]},style:{stroke:o.get("dataBackgroundColor")||o.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}}))),MM([0,1],function(t){var r=fr(o.get("handleIcon"),{cursor:$c(this._orient),draggable:!0,drift:bM(this._onDragMove,this,t),onmousemove:function(t){tm(t.event)},ondragend:bM(this._onDragEnd,this),onmouseover:bM(this._showDataInfo,this,!0),onmouseout:bM(this._showDataInfo,this,!1)},{x:-1,y:0,width:2,height:2}),a=r.getBoundingRect();this._handleHeight=_r(o.get("handleSize"),this._size[1]),this._handleWidth=a.width/a.height*this._handleHeight,r.setStyle(o.getModel("handleStyle").getItemStyle());var s=o.get("handleColor");null!=s&&(r.style.fill=s),i.add(e[t]=r);var l=o.textStyleModel;this.group.add(n[t]=new kv({silent:!0,invisible:!0,style:{x:0,y:0,text:"",textVerticalAlign:"middle",textAlign:"center",textFill:l.getTextColor(),textFont:l.getFont()},z2:10}))},this)},_resetInterval:function(){var t=this._range=this.dataZoomModel.getPercentRange(),e=this._getViewExtent();this._handleEnds=[_M(t[0],[0,100],e,!0),_M(t[1],[0,100],e,!0)]},_updateInterval:function(t,e){var n=this.dataZoomModel,i=this._handleEnds,r=this._getViewExtent(),o=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];yM(e,i,r,n.get("zoomLock")?"all":t,null!=o.minSpan?_M(o.minSpan,a,r,!0):null,null!=o.maxSpan?_M(o.maxSpan,a,r,!0):null);var s=this._range,l=this._range=wM([_M(i[0],r,a,!0),_M(i[1],r,a,!0)]);return!s||s[0]!==l[0]||s[1]!==l[1]},_updateView:function(t){var e=this._displayables,n=this._handleEnds,i=wM(n.slice()),r=this._size;MM([0,1],function(t){var i=e.handles[t],o=this._handleHeight;i.attr({scale:[o/2,o/2],position:[n[t],r[1]/2-o/2]})},this),e.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:r[1]}),this._updateDataInfo(t)},_updateDataInfo:function(t){function e(t){var e=lr(i.handles[t].parent,this.group),n=hr(0===t?"right":"left",e),s=this._handleWidth/2+IM,l=ur([c[t]+(0===t?-s:s),this._size[1]/2],e);r[t].setStyle({x:l[0],y:l[1],textVerticalAlign:o===SM?"middle":n,textAlign:o===SM?n:"center",text:a[t]})}var n=this.dataZoomModel,i=this._displayables,r=i.handleLabels,o=this._orient,a=["",""];if(n.get("showDetail")){var s=n.findRepresentativeAxisProxy();if(s){var l=s.getAxisModel().axis,u=this._range,h=t?s.calculateDataWindow({start:u[0],end:u[1]}).valueWindow:s.getDataValueWindow();a=[this._formatLabel(h[0],l),this._formatLabel(h[1],l)]}}var c=wM(this._handleEnds.slice());e.call(this,0),e.call(this,1)},_formatLabel:function(t,e){var n=this.dataZoomModel,i=n.get("labelFormatter"),r=n.get("labelPrecision");null!=r&&"auto"!==r||(r=e.getPixelPrecision());var o=null==t||isNaN(t)?"":"category"===e.type||"time"===e.type?e.scale.getLabel(Math.round(t)):t.toFixed(Math.min(r,20));return x(i)?i(t,o):_(i)?i.replace("{value}",o):o},_showDataInfo:function(t){t=this._dragging||t;var e=this._displayables.handleLabels;e[0].attr("invisible",!t),e[1].attr("invisible",!t)},_onDragMove:function(t,e,n){this._dragging=!0;var i=ur([e,n],this._displayables.barGroup.getLocalTransform(),!0),r=this._updateInterval(t,i[0]),o=this.dataZoomModel.get("realtime");this._updateView(!o),r&&o&&this._dispatchZoomAction()},_onDragEnd:function(){this._dragging=!1,this._showDataInfo(!1),!this.dataZoomModel.get("realtime")&&this._dispatchZoomAction()},_onClickPanelClick:function(t){var e=this._size,n=this._displayables.barGroup.transformCoordToLocal(t.offsetX,t.offsetY);if(!(n[0]<0||n[0]>e[0]||n[1]<0||n[1]>e[1])){var i=this._handleEnds,r=(i[0]+i[1])/2,o=this._updateInterval("all",n[0]-r);this._updateView(),o&&this._dispatchZoomAction()}},_dispatchZoomAction:function(){var t=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,start:t[0],end:t[1]})},_findCoordRect:function(){var t;if(MM(this.getTargetCoordInfo(),function(e){if(!t&&e.length){var n=e[0].model.coordinateSystem;t=n.getRect&&n.getRect()}}),!t){var e=this.api.getWidth(),n=this.api.getHeight();t={x:.2*e,y:.2*n,width:.6*e,height:.6*n}}return t}});mM.extend({type:"dataZoom.inside",defaultOption:{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,preventDefaultMouseMove:!0}});var DM="\0_ec_interaction_mutex";ts({type:"takeGlobalCursor",event:"globalCursorTaken",update:"update"},function(){}),h(ed,Zp);var AM=v,kM="\0_ec_dataZoom_roams",PM=m,LM=vM.extend({type:"dataZoom.inside",init:function(t,e){this._range},render:function(t,e,n,i){LM.superApply(this,"render",arguments),this._range=t.getPercentRange(),d(this.getTargetCoordInfo(),function(e,i){var r=f(e,function(t){return cd(t.model)});d(e,function(e){var o=e.model,a=t.option;ud(n,{coordId:cd(o),allCoordIds:r,containsPoint:function(t,e,n){return o.coordinateSystem.containPoint([e,n])},dataZoomId:t.id,throttleRate:t.get("throttle",!0),panGetRange:PM(this._onPan,this,e,i),zoomGetRange:PM(this._onZoom,this,e,i),zoomLock:a.zoomLock,disabled:a.disabled,roamControllerOpt:{zoomOnMouseWheel:a.zoomOnMouseWheel,moveOnMouseMove:a.moveOnMouseMove,preventDefaultMouseMove:a.preventDefaultMouseMove}})},this)},this)},dispose:function(){hd(this.api,this.dataZoomModel.id),LM.superApply(this,"dispose",arguments),this._range=null},_onPan:function(t,e,n,i,r,o,a,s,l){var u=this._range,h=u.slice(),c=t.axisModels[0];if(c){var d=OM[e]([o,a],[s,l],c,n,t),f=d.signal*(h[1]-h[0])*d.pixel/d.pixelLength;return yM(f,h,[0,100],"all"),this._range=h,u[0]!==h[0]||u[1]!==h[1]?h:void 0}},_onZoom:function(t,e,n,i,r,o){var a=this._range,s=a.slice(),l=t.axisModels[0];if(l){var u=OM[e](null,[r,o],l,n,t),h=(u.signal>0?u.pixelStart+u.pixelLength-u.pixel:u.pixel-u.pixelStart)/u.pixelLength*(s[1]-s[0])+s[0];i=Math.max(1/i,0),s[0]=(s[0]-h)*i+h,s[1]=(s[1]-h)*i+h;var c=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return yM(0,s,[0,100],0,c.minSpan,c.maxSpan),this._range=s,a[0]!==s[0]||a[1]!==s[1]?s:void 0}}}),OM={grid:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem.getRect();return t=t||[0,0],"x"===o.dim?(a.pixel=e[0]-t[0],a.pixelLength=s.width,a.pixelStart=s.x,a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=s.height,a.pixelStart=s.y,a.signal=o.inverse?-1:1),a},polar:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return t=t?s.pointToCoord(t):[0,0],e=s.pointToCoord(e),"radiusAxis"===n.mainType?(a.pixel=e[0]-t[0],a.pixelLength=l[1]-l[0],a.pixelStart=l[0],a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=u[1]-u[0],a.pixelStart=u[0],a.signal=o.inverse?-1:1),a},singleAxis:function(t,e,n,i,r){var o=n.axis,a=r.model.coordinateSystem.getRect(),s={};return t=t||[0,0],"horizontal"===o.orient?(s.pixel=e[0]-t[0],s.pixelLength=a.width,s.pixelStart=a.x,s.signal=o.inverse?1:-1):(s.pixel=e[1]-t[1],s.pixelLength=a.height,s.pixelStart=a.y,s.signal=o.inverse?-1:1),s}};Ja({getTargetSeries:function(t){var e=N();return t.eachComponent("dataZoom",function(t){t.eachTargetAxis(function(t,n,i){d(i.getAxisProxy(t.name,n).getTargetSeriesModels(),function(t){e.set(t.uid,t)})})}),e},modifyOutputEnd:!0,overallReset:function(t,e){t.eachComponent("dataZoom",function(t){t.eachTargetAxis(function(t,n,i){i.getAxisProxy(t.name,n).reset(i,e)}),t.eachTargetAxis(function(t,n,i){i.getAxisProxy(t.name,n).filterData(i,e)})}),t.eachComponent("dataZoom",function(t){var e=t.findRepresentativeAxisProxy(),n=e.getDataPercentWindow(),i=e.getDataValueWindow();t.setRawRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]},!0)})}}),ts("dataZoom",function(t,e){var n=Fc(m(e.eachComponent,e,"dataZoom"),hM,function(t,e){return t.get(e.axisIndex)}),i=[];e.eachComponent({mainType:"dataZoom",query:t},function(t,e){i.push.apply(i,n(t).nodes)}),d(i,function(e,n){e.setRawRange({start:t.start,end:t.end,startValue:t.startValue,endValue:t.endValue})})});var zM={},EM=os({type:"toolbox",layoutMode:{type:"box",ignoreSize:!0},optionUpdated:function(){EM.superApply(this,"optionUpdated",arguments),d(this.option.feature,function(t,e){var n=wd(e);n&&i(t,n.defaultOption)})},defaultOption:{show:!0,z:6,zlevel:0,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}}}});as({type:"toolbox",render:function(t,e,n,i){function r(r,a){var s,c=h[r],d=h[a],f=new pr(l[c],t,t.ecModel);if(c&&!d){if(bd(c))s={model:f,onclick:f.option.onclick,featureName:c};else{var p=wd(c);if(!p)return;s=new p(f,e,n)}u[c]=s}else{if(!(s=u[d]))return;s.model=f,s.ecModel=e,s.api=n}c||!d?f.get("show")&&!s.unusable?(o(f,s,c),f.setIconStatus=function(t,e){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[t]=e,i[t]&&i[t].trigger(e)},s.render&&s.render(f,e,n,i)):s.remove&&s.remove(e,n):s.dispose&&s.dispose(e,n)}function o(i,r,o){var l=i.getModel("iconStyle"),u=i.getModel("emphasis.iconStyle"),h=r.getIcons?r.getIcons():i.get("icon"),c=i.get("title")||{};if("string"==typeof h){var f=h,p=c;c={},(h={})[o]=f,c[o]=p}var g=i.iconPaths={};d(h,function(o,h){var d=fr(o,{},{x:-s/2,y:-s/2,width:s,height:s});d.setStyle(l.getItemStyle()),d.hoverStyle=u.getItemStyle(),qi(d),t.get("showTitle")&&(d.__title=c[h],d.on("mouseover",function(){var t=u.getItemStyle();d.setStyle({text:c[h],textPosition:t.textPosition||"bottom",textFill:t.fill||t.stroke||"#000",textAlign:t.textAlign||"center"})}).on("mouseout",function(){d.setStyle({textFill:null})})),d.trigger(i.get("iconStatus."+h)||"normal"),a.add(d),d.on("click",m(r.onclick,r,e,n,h)),g[h]=d})}var a=this.group;if(a.removeAll(),t.get("show")){var s=+t.get("itemSize"),l=t.get("feature")||{},u=this._features||(this._features={}),h=[];d(l,function(t,e){h.push(e)}),new hs(this._featureNames||[],h).add(r).update(r).remove(v(r,null)).execute(),this._featureNames=h,Jh(a,t,n),a.add(tc(a.getBoundingRect(),t)),a.eachChild(function(t){var e=t.__title,i=t.hoverStyle;if(i&&e){var r=ce(e,Ce(i)),o=t.position[0]+a.position[0],l=!1;t.position[1]+a.position[1]+s+r.height>n.getHeight()&&(i.textPosition="top",l=!0);var u=l?-5-r.height:s+8;o+r.width/2>n.getWidth()?(i.textPosition=["100%",u],i.textAlign="right"):o-r.width/2<0&&(i.textPosition=[0,u],i.textAlign="left")}})}},updateView:function(t,e,n,i){d(this._features,function(t){t.updateView&&t.updateView(t.model,e,n,i)})},remove:function(t,e){d(this._features,function(n){n.remove&&n.remove(t,e)}),this.group.removeAll()},dispose:function(t,e){d(this._features,function(n){n.dispose&&n.dispose(t,e)})}});var NM=Sx.toolbox.saveAsImage;Md.defaultOption={show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:NM.title,type:"png",name:"",excludeComponents:["toolbox"],pixelRatio:1,lang:NM.lang.slice()},Md.prototype.unusable=!bp.canvasSupported,Md.prototype.onclick=function(t,e){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",r=document.createElement("a"),o=n.get("type",!0)||"png";r.download=i+"."+o,r.target="_blank";var a=e.getConnectedDataURL({type:o,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||"#fff",excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")});if(r.href=a,"function"!=typeof MouseEvent||bp.browser.ie||bp.browser.edge)if(window.navigator.msSaveOrOpenBlob){for(var s=atob(a.split(",")[1]),l=s.length,u=new Uint8Array(l);l--;)u[l]=s.charCodeAt(l);var h=new Blob([u]);window.navigator.msSaveOrOpenBlob(h,i+"."+o)}else{var c=n.get("lang"),d='<body style="margin:0;"><img src="'+a+'" style="max-width:100%;" title="'+(c&&c[0]||"")+'" /></body>';window.open().document.write(d)}else{var f=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!1});r.dispatchEvent(f)}},_d("saveAsImage",Md);var RM=Sx.toolbox.magicType;Sd.defaultOption={show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z",tiled:"M2.3,2.2h22.8V25H2.3V2.2z M35,2.2h22.8V25H35V2.2zM2.3,35h22.8v22.8H2.3V35z M35,35h22.8v22.8H35V35z"},title:n(RM.title),option:{},seriesIndex:{}};var BM=Sd.prototype;BM.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return d(t.get("type"),function(t){e[t]&&(n[t]=e[t])}),n};var VM={line:function(t,e,n,r){if("bar"===t)return i({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},r.get("option.line")||{},!0)},bar:function(t,e,n,r){if("line"===t)return i({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},r.get("option.bar")||{},!0)},stack:function(t,e,n,r){if("line"===t||"bar"===t)return i({id:e,stack:"__ec_magicType_stack__"},r.get("option.stack")||{},!0)},tiled:function(t,e,n,r){if("line"===t||"bar"===t)return i({id:e,stack:""},r.get("option.tiled")||{},!0)}},FM=[["line","bar"],["stack","tiled"]];BM.onclick=function(t,e,n){var i=this.model,r=i.get("seriesIndex."+n);if(VM[n]){var o={series:[]};d(FM,function(t){l(t,n)>=0&&d(t,function(t){i.setIconStatus(t,"normal")})}),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},function(e){var r=e.subType,s=e.id,l=VM[n](r,s,e,i);l&&(a(l,e.option),o.series.push(l));var u=e.coordinateSystem;if(u&&"cartesian2d"===u.type&&("line"===n||"bar"===n)){var h=u.getAxesByScale("ordinal")[0];if(h){var c=h.dim+"Axis",d=t.queryComponents({mainType:c,index:e.get(name+"Index"),id:e.get(name+"Id")})[0].componentIndex;o[c]=o[c]||[];for(var f=0;f<=d;f++)o[c][d]=o[c][d]||{};o[c][d].boundaryGap="bar"===n}}}),e.dispatchAction({type:"changeMagicType",currentType:n,newOption:o})}},ts({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},function(t,e){e.mergeOption(t.newOption)}),_d("magicType",Sd);var HM=Sx.toolbox.dataView,GM=new Array(60).join("-"),WM="\t",ZM=new RegExp("["+WM+"]+","g");zd.defaultOption={show:!0,readOnly:!1,optionToContent:null,contentToOption:null,icon:"M17.5,17.3H33 M17.5,17.3H33 M45.4,29.5h-28 M11.5,2v56H51V14.8L38.4,2H11.5z M38.4,2.2v12.7H51 M45.4,41.7h-28",title:n(HM.title),lang:n(HM.lang),backgroundColor:"#fff",textColor:"#000",textareaColor:"#fff",textareaBorderColor:"#333",buttonColor:"#c23531",buttonTextColor:"#fff"},zd.prototype.onclick=function(t,e){function n(){i.removeChild(o),x._dom=null}var i=e.getDom(),r=this.model;this._dom&&i.removeChild(this._dom);var o=document.createElement("div");o.style.cssText="position:absolute;left:5px;top:5px;bottom:5px;right:5px;",o.style.backgroundColor=r.get("backgroundColor")||"#fff";var a=document.createElement("h4"),s=r.get("lang")||[];a.innerHTML=s[0]||r.get("title"),a.style.cssText="margin: 10px 20px;",a.style.color=r.get("textColor");var l=document.createElement("div"),u=document.createElement("textarea");l.style.cssText="display:block;width:100%;overflow:auto;";var h=r.get("optionToContent"),c=r.get("contentToOption"),d=Dd(t);if("function"==typeof h){var f=h(e.getOption());"string"==typeof f?l.innerHTML=f:S(f)&&l.appendChild(f)}else l.appendChild(u),u.readOnly=r.get("readOnly"),u.style.cssText="width:100%;height:100%;font-family:monospace;font-size:14px;line-height:1.6rem;",u.style.color=r.get("textColor"),u.style.borderColor=r.get("textareaBorderColor"),u.style.backgroundColor=r.get("textareaColor"),u.value=d.value;var p=d.meta,g=document.createElement("div");g.style.cssText="position:absolute;bottom:0;left:0;right:0;";var m="float:right;margin-right:20px;border:none;cursor:pointer;padding:2px 5px;font-size:12px;border-radius:3px",v=document.createElement("div"),y=document.createElement("div");m+=";background-color:"+r.get("buttonColor"),m+=";color:"+r.get("buttonTextColor");var x=this;on(v,"click",n),on(y,"click",function(){var t;try{t="function"==typeof c?c(l,e.getOption()):Od(u.value,p)}catch(t){throw n(),new Error("Data view format error "+t)}t&&e.dispatchAction({type:"changeDataView",newOption:t}),n()}),v.innerHTML=s[1],y.innerHTML=s[2],y.style.cssText=m,v.style.cssText=m,!r.get("readOnly")&&g.appendChild(y),g.appendChild(v),on(u,"keydown",function(t){if(9===(t.keyCode||t.which)){var e=this.value,n=this.selectionStart,i=this.selectionEnd;this.value=e.substring(0,n)+WM+e.substring(i),this.selectionStart=this.selectionEnd=n+1,tm(t)}}),o.appendChild(a),o.appendChild(l),o.appendChild(g),l.style.height=i.clientHeight-80+"px",i.appendChild(o),this._dom=o},zd.prototype.remove=function(t,e){this._dom&&e.getDom().removeChild(this._dom)},zd.prototype.dispose=function(t,e){this.remove(t,e)},_d("dataView",zd),ts({type:"changeDataView",event:"dataViewChanged",update:"prepareAndUpdate"},function(t,e){var n=[];d(t.newOption.series,function(t){var i=e.getSeriesByName(t.name)[0];if(i){var r=i.get("data");n.push({name:t.name,data:Ed(t.data,r)})}else n.push(o({type:"scatter"},t))}),e.mergeOption(a({series:n},t.newOption))});var UM=v,XM=d,jM=f,YM=Math.min,qM=Math.max,$M=Math.pow,KM=1e4,QM=6,JM=6,tS="globalPan",eS={w:[0,0],e:[0,1],n:[1,0],s:[1,1]},nS={w:"ew",e:"ew",n:"ns",s:"ns",ne:"nesw",sw:"nesw",nw:"nwse",se:"nwse"},iS={brushStyle:{lineWidth:2,stroke:"rgba(0,0,0,0.3)",fill:"rgba(0,0,0,0.1)"},transformable:!0,brushMode:"single",removeOnClick:!1},rS=0;Nd.prototype={constructor:Nd,enableBrush:function(t){return this._brushType&&Bd(this),t.brushType&&Rd(this,t),this},setPanels:function(t){if(t&&t.length){var e=this._panels={};d(t,function(t){e[t.panelId]=n(t)})}else this._panels=null;return this},mount:function(t){t=t||{},this._enableGlobalPan=t.enableGlobalPan;var e=this.group;return this._zr.add(e),e.attr({position:t.position||[0,0],rotation:t.rotation||0,scale:t.scale||[1,1]}),this._transform=e.getLocalTransform(),this},eachCover:function(t,e){XM(this._covers,t,e)},updateCovers:function(t){function e(t,e){return(null!=t.id?t.id:o+e)+"-"+t.brushType}function r(e,n){var i=t[e];if(null!=n&&a[n]===u)s[e]=a[n];else{var r=s[e]=null!=n?(a[n].__brushOption=i,a[n]):Fd(l,Vd(l,i));Wd(l,r)}}t=f(t,function(t){return i(n(iS),t,!0)});var o="\0-brush-index-",a=this._covers,s=this._covers=[],l=this,u=this._creatingCover;return new hs(a,t,function(t,n){return e(t.__brushOption,n)},e).add(r).update(r).remove(function(t){a[t]!==u&&l.group.remove(a[t])}).execute(),this},unmount:function(){return this.enableBrush(!1),jd(this),this._zr.remove(this.group),this},dispose:function(){this.unmount(),this.off()}},h(Nd,Zp);var oS={mousedown:function(t){if(this._dragging)mf.call(this,t);else if(!t.target||!t.target.draggable){df(t);var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);this._creatingCover=null,(this._creatingPanel=Ud(this,t,e))&&(this._dragging=!0,this._track=[e.slice()])}},mousemove:function(t){var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);if(cf(this,t,e),this._dragging){df(t);var n=pf(this,t,e,!1);n&&Yd(this,n)}},mouseup:mf},aS={lineX:vf(0),lineY:vf(1),rect:{createCover:function(t,e){return Kd(UM(af,function(t){return t},function(t){return t}),t,e,["w","e","n","s","se","sw","ne","nw"])},getCreatingRange:function(t){var e=$d(t);return nf(e[1][0],e[1][1],e[0][0],e[0][1])},updateCoverShape:function(t,e,n,i){Qd(t,e,n,i)},updateCommon:Jd,contain:ff},polygon:{createCover:function(t,e){var n=new Sg;return n.add(new Vv({name:"main",style:ef(e),silent:!0})),n},getCreatingRange:function(t){return t},endCreating:function(t,e){e.remove(e.childAt(0)),e.add(new Bv({name:"main",draggable:!0,drift:UM(sf,t,e),ondragend:UM(Yd,t,{isEnd:!0})}))},updateCoverShape:function(t,e,n,i){e.childAt(0).setShape({points:uf(t,e,n)})},updateCommon:Jd,contain:ff}},sS={axisPointer:1,tooltip:1,brush:1},lS=d,uS=l,hS=v,cS=["dataToPoint","pointToData"],dS=["grid","xAxis","yAxis","geo","graph","polar","radiusAxis","angleAxis","bmap"],fS=Mf.prototype;fS.setOutputRanges=function(t,e){this.matchOutputRanges(t,e,function(t,e,n){if((t.coordRanges||(t.coordRanges=[])).push(e),!t.coordRange){t.coordRange=e;var i=vS[t.brushType](0,n,e);t.__rangeOffset={offset:yS[t.brushType](i.values,t.range,[1,1]),xyMinMax:i.xyMinMax}}})},fS.matchOutputRanges=function(t,e,n){lS(t,function(t){var i=this.findTargetInfo(t,e);i&&!0!==i&&d(i.coordSyses,function(i){var r=vS[t.brushType](1,i,t.range);n(t,r.values,i,e)})},this)},fS.setInputRanges=function(t,e){lS(t,function(t){var n=this.findTargetInfo(t,e);if(t.range=t.range||[],n&&!0!==n){t.panelId=n.panelId;var i=vS[t.brushType](0,n.coordSys,t.coordRange),r=t.__rangeOffset;t.range=r?yS[t.brushType](i.values,r.offset,Df(i.xyMinMax,r.xyMinMax)):i.values}},this)},fS.makePanelOpts=function(t,e){return f(this._targetInfoList,function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:e&&e(n),clipPath:xf(i),isTargetByCursor:wf(i,t,n.coordSysModel),getLinearBrushOtherExtent:_f(i)}})},fS.controlSeries=function(t,e,n){var i=this.findTargetInfo(t,n);return!0===i||i&&uS(i.coordSyses,e.coordinateSystem)>=0},fS.findTargetInfo=function(t,e){for(var n=this._targetInfoList,i=If(e,t),r=0;r<n.length;r++){var o=n[r],a=t.panelId;if(a){if(o.panelId===a)return o}else for(r=0;r<gS.length;r++)if(gS[r](i,o))return o}return!0};var pS={grid:function(t,e){var n=t.xAxisModels,i=t.yAxisModels,r=t.gridModels,o=N(),a={},s={};(n||i||r)&&(lS(n,function(t){var e=t.axis.grid.model;o.set(e.id,e),a[e.id]=!0}),lS(i,function(t){var e=t.axis.grid.model;o.set(e.id,e),s[e.id]=!0}),lS(r,function(t){o.set(t.id,t),a[t.id]=!0,s[t.id]=!0}),o.each(function(t){var r=t.coordinateSystem,o=[];lS(r.getCartesians(),function(t,e){(uS(n,t.getAxis("x").model)>=0||uS(i,t.getAxis("y").model)>=0)&&o.push(t)}),e.push({panelId:"grid--"+t.id,gridModel:t,coordSysModel:t,coordSys:o[0],coordSyses:o,getPanelRect:mS.grid,xAxisDeclared:a[t.id],yAxisDeclared:s[t.id]})}))},geo:function(t,e){lS(t.geoModels,function(t){var n=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:n,coordSyses:[n],getPanelRect:mS.geo})})}},gS=[function(t,e){var n=t.xAxisModel,i=t.yAxisModel,r=t.gridModel;return!r&&n&&(r=n.axis.grid.model),!r&&i&&(r=i.axis.grid.model),r&&r===e.gridModel},function(t,e){var n=t.geoModel;return n&&n===e.geoModel}],mS={grid:function(){return this.coordSys.grid.getRect().clone()},geo:function(){var t=this.coordSys,e=t.getBoundingRect().clone();return e.applyTransform(lr(t)),e}},vS={lineX:hS(Cf,0),lineY:hS(Cf,1),rect:function(t,e,n){var i=e[cS[t]]([n[0][0],n[1][0]]),r=e[cS[t]]([n[0][1],n[1][1]]),o=[Sf([i[0],r[0]]),Sf([i[1],r[1]])];return{values:o,xyMinMax:o}},polygon:function(t,e,n){var i=[[1/0,-1/0],[1/0,-1/0]];return{values:f(n,function(n){var r=e[cS[t]](n);return i[0][0]=Math.min(i[0][0],r[0]),i[1][0]=Math.min(i[1][0],r[1]),i[0][1]=Math.max(i[0][1],r[0]),i[1][1]=Math.max(i[1][1],r[1]),r}),xyMinMax:i}}},yS={lineX:hS(Tf,0),lineY:hS(Tf,1),rect:function(t,e,n){return[[t[0][0]-n[0]*e[0][0],t[0][1]-n[0]*e[0][1]],[t[1][0]-n[1]*e[1][0],t[1][1]-n[1]*e[1][1]]]},polygon:function(t,e,n){return f(t,function(t,i){return[t[0]-n[0]*e[i][0],t[1]-n[1]*e[i][1]]})}},xS=d,_S="\0_ec_hist_store";mM.extend({type:"dataZoom.select"}),vM.extend({type:"dataZoom.select"});var wS=Sx.toolbox.dataZoom,bS=d,MS="\0_ec_\0toolbox-dataZoom_";Ef.defaultOption={show:!0,icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:n(wS.title)};var SS=Ef.prototype;SS.render=function(t,e,n,i){this.model=t,this.ecModel=e,this.api=n,Bf(t,e,this,i,n),Rf(t,e)},SS.onclick=function(t,e,n){IS[n].call(this)},SS.remove=function(t,e){this._brushController.unmount()},SS.dispose=function(t,e){this._brushController.dispose()};var IS={zoom:function(){var t=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:t})},back:function(){this._dispatchZoomAction(Pf(this.ecModel))}};SS._onBrush=function(t,e){function n(t,e,n){var a=e.getAxis(t),s=a.model,l=i(t,s,o),u=l.findRepresentativeAxisProxy(s).getMinMaxSpan();null==u.minValueSpan&&null==u.maxValueSpan||(n=yM(0,n.slice(),a.scale.getExtent(),0,u.minValueSpan,u.maxValueSpan)),l&&(r[l.id]={dataZoomId:l.id,startValue:n[0],endValue:n[1]})}function i(t,e,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},function(n){n.getAxisModel(t,e.componentIndex)&&(i=n)}),i}if(e.isEnd&&t.length){var r={},o=this.ecModel;this._brushController.updateCovers([]),new Mf(Nf(this.model.option),o,{include:["grid"]}).matchOutputRanges(t,o,function(t,e,i){if("cartesian2d"===i.type){var r=t.brushType;"rect"===r?(n("x",i,e[0]),n("y",i,e[1])):n({lineX:"x",lineY:"y"}[r],i,e)}}),kf(o,r),this._dispatchZoomAction(r)}},SS._dispatchZoomAction=function(t){var e=[];bS(t,function(t,i){e.push(n(t))}),e.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:e})},_d("dataZoom",Ef),Qa(function(t){function e(t,e){if(e){var r=t+"Index",o=e[r];null==o||"all"==o||y(o)||(o=!1===o||"none"===o?[]:[o]),n(t,function(e,n){if(null==o||"all"==o||-1!==l(o,n)){var a={type:"select",$fromToolbox:!0,id:MS+t+n};a[r]=n,i.push(a)}})}}function n(e,n){var i=t[e];y(i)||(i=i?[i]:[]),bS(i,n)}if(t){var i=t.dataZoom||(t.dataZoom=[]);y(i)||(t.dataZoom=i=[i]);var r=t.toolbox;if(r&&(y(r)&&(r=r[0]),r&&r.feature)){var o=r.feature.dataZoom;e("xAxis",o),e("yAxis",o)}}});var CS=Sx.toolbox.restore;Vf.defaultOption={show:!0,icon:"M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5",title:CS.title},Vf.prototype.onclick=function(t,e,n){Lf(t),e.dispatchAction({type:"restore",from:this.uid})},_d("restore",Vf),ts({type:"restore",event:"restore",update:"prepareAndUpdate"},function(t,e){e.resetOption("recreate")});var TS,DS="urn:schemas-microsoft-com:vml",AS="undefined"==typeof window?null:window,kS=!1,PS=AS&&AS.document;if(PS&&!bp.canvasSupported)try{!PS.namespaces.zrvml&&PS.namespaces.add("zrvml",DS),TS=function(t){return PS.createElement("<zrvml:"+t+' class="zrvml">')}}catch(t){TS=function(t){return PS.createElement("<"+t+' xmlns="'+DS+'" class="zrvml">')}}var LS=av.CMD,OS=Math.round,zS=Math.sqrt,ES=Math.abs,NS=Math.cos,RS=Math.sin,BS=Math.max;if(!bp.canvasSupported){var VS=21600,FS=VS/2,HS=function(t){t.style.cssText="position:absolute;left:0;top:0;width:1px;height:1px;",t.coordsize=VS+","+VS,t.coordorigin="0,0"},GS=function(t){return String(t).replace(/&/g,"&").replace(/"/g,""")},WS=function(t,e,n){return"rgb("+[t,e,n].join(",")+")"},ZS=function(t,e){e&&t&&e.parentNode!==t&&t.appendChild(e)},US=function(t,e){e&&t&&e.parentNode===t&&t.removeChild(e)},XS=function(t,e,n){return 1e5*(parseFloat(t)||0)+1e3*(parseFloat(e)||0)+n},jS=function(t,e){return"string"==typeof t?t.lastIndexOf("%")>=0?parseFloat(t)/100*e:parseFloat(t):t},YS=function(t,e,n){var i=St(e);n=+n,isNaN(n)&&(n=1),i&&(t.color=WS(i[0],i[1],i[2]),t.opacity=n*i[3])},qS=function(t){var e=St(t);return[WS(e[0],e[1],e[2]),e[3]]},$S=function(t,e,n){var i=e.fill;if(null!=i)if(i instanceof Xv){var r,o=0,a=[0,0],s=0,l=1,u=n.getBoundingRect(),h=u.width,c=u.height;if("linear"===i.type){r="gradient";var d=n.transform,f=[i.x*h,i.y*c],p=[i.x2*h,i.y2*c];d&&($(f,f,d),$(p,p,d));var g=p[0]-f[0],m=p[1]-f[1];(o=180*Math.atan2(g,m)/Math.PI)<0&&(o+=360),o<1e-6&&(o=0)}else{r="gradientradial";var f=[i.x*h,i.y*c],d=n.transform,v=n.scale,y=h,x=c;a=[(f[0]-u.x)/y,(f[1]-u.y)/x],d&&$(f,f,d),y/=v[0]*VS,x/=v[1]*VS;var _=BS(y,x);s=0/_,l=2*i.r/_-s}var w=i.colorStops.slice();w.sort(function(t,e){return t.offset-e.offset});for(var b=w.length,M=[],S=[],I=0;I<b;I++){var C=w[I],T=qS(C.color);S.push(C.offset*l+s+" "+T[0]),0!==I&&I!==b-1||M.push(T)}if(b>=2){var D=M[0][0],A=M[1][0],k=M[0][1]*e.opacity,P=M[1][1]*e.opacity;t.type=r,t.method="none",t.focus="100%",t.angle=o,t.color=D,t.color2=A,t.colors=S.join(","),t.opacity=P,t.opacity2=k}"radial"===r&&(t.focusposition=a.join(","))}else YS(t,i,e.opacity)},KS=function(t,e){null!=e.lineDash&&(t.dashstyle=e.lineDash.join(" ")),null==e.stroke||e.stroke instanceof Xv||YS(t,e.stroke,e.opacity)},QS=function(t,e,n,i){var r="fill"==e,o=t.getElementsByTagName(e)[0];null!=n[e]&&"none"!==n[e]&&(r||!r&&n.lineWidth)?(t[r?"filled":"stroked"]="true",n[e]instanceof Xv&&US(t,o),o||(o=Ff(e)),r?$S(o,n,i):KS(o,n),ZS(t,o)):(t[r?"filled":"stroked"]="false",US(t,o))},JS=[[],[],[]],tI=function(t,e){var n,i,r,o,a,s,l=LS.M,u=LS.C,h=LS.L,c=LS.A,d=LS.Q,f=[],p=t.data,g=t.len();for(o=0;o<g;){switch(r=p[o++],i="",n=0,r){case l:i=" m ",n=1,a=p[o++],s=p[o++],JS[0][0]=a,JS[0][1]=s;break;case h:i=" l ",n=1,a=p[o++],s=p[o++],JS[0][0]=a,JS[0][1]=s;break;case d:case u:i=" c ",n=3;var m,v,y=p[o++],x=p[o++],_=p[o++],w=p[o++];r===d?(m=_,v=w,_=(_+2*y)/3,w=(w+2*x)/3,y=(a+2*y)/3,x=(s+2*x)/3):(m=p[o++],v=p[o++]),JS[0][0]=y,JS[0][1]=x,JS[1][0]=_,JS[1][1]=w,JS[2][0]=m,JS[2][1]=v,a=m,s=v;break;case c:var b=0,M=0,S=1,I=1,C=0;e&&(b=e[4],M=e[5],S=zS(e[0]*e[0]+e[1]*e[1]),I=zS(e[2]*e[2]+e[3]*e[3]),C=Math.atan2(-e[1]/I,e[0]/S));var T=p[o++],D=p[o++],A=p[o++],k=p[o++],P=p[o++]+C,L=p[o++]+P+C;o++;var O=p[o++],z=T+NS(P)*A,E=D+RS(P)*k,y=T+NS(L)*A,x=D+RS(L)*k,N=O?" wa ":" at ";Math.abs(z-y)<1e-4&&(Math.abs(L-P)>.01?O&&(z+=.0125):Math.abs(E-D)<1e-4?O&&z<T||!O&&z>T?x-=.0125:x+=.0125:O&&E<D||!O&&E>D?y+=.0125:y-=.0125),f.push(N,OS(((T-A)*S+b)*VS-FS),",",OS(((D-k)*I+M)*VS-FS),",",OS(((T+A)*S+b)*VS-FS),",",OS(((D+k)*I+M)*VS-FS),",",OS((z*S+b)*VS-FS),",",OS((E*I+M)*VS-FS),",",OS((y*S+b)*VS-FS),",",OS((x*I+M)*VS-FS)),a=y,s=x;break;case LS.R:var R=JS[0],B=JS[1];R[0]=p[o++],R[1]=p[o++],B[0]=R[0]+p[o++],B[1]=R[1]+p[o++],e&&($(R,R,e),$(B,B,e)),R[0]=OS(R[0]*VS-FS),B[0]=OS(B[0]*VS-FS),R[1]=OS(R[1]*VS-FS),B[1]=OS(B[1]*VS-FS),f.push(" m ",R[0],",",R[1]," l ",B[0],",",R[1]," l ",B[0],",",B[1]," l ",R[0],",",B[1]);break;case LS.Z:f.push(" x ")}if(n>0){f.push(i);for(var V=0;V<n;V++){var F=JS[V];e&&$(F,F,e),f.push(OS(F[0]*VS-FS),",",OS(F[1]*VS-FS),V<n-1?",":"")}}}return f.join("")};xi.prototype.brushVML=function(t){var e=this.style,n=this._vmlEl;n||(n=Ff("shape"),HS(n),this._vmlEl=n),QS(n,"fill",e,this),QS(n,"stroke",e,this);var i=this.transform,r=null!=i,o=n.getElementsByTagName("stroke")[0];if(o){var a=e.lineWidth;if(r&&!e.strokeNoScale){var s=i[0]*i[3]-i[1]*i[2];a*=zS(ES(s))}o.weight=a+"px"}var l=this.path||(this.path=new av);this.__dirtyPath&&(l.beginPath(),this.buildPath(l,this.shape),l.toStatic(),this.__dirtyPath=!1),n.path=tI(l,this.transform),n.style.zIndex=XS(this.zlevel,this.z,this.z2),ZS(t,n),null!=e.text?this.drawRectText(t,this.getBoundingRect()):this.removeRectText(t)},xi.prototype.onRemove=function(t){US(t,this._vmlEl),this.removeRectText(t)},xi.prototype.onAdd=function(t){ZS(t,this._vmlEl),this.appendRectText(t)};var eI=function(t){return"object"==typeof t&&t.tagName&&"IMG"===t.tagName.toUpperCase()};je.prototype.brushVML=function(t){var e,n,i=this.style,r=i.image;if(eI(r)){var o=r.src;if(o===this._imageSrc)e=this._imageWidth,n=this._imageHeight;else{var a=r.runtimeStyle,s=a.width,l=a.height;a.width="auto",a.height="auto",e=r.width,n=r.height,a.width=s,a.height=l,this._imageSrc=o,this._imageWidth=e,this._imageHeight=n}r=o}else r===this._imageSrc&&(e=this._imageWidth,n=this._imageHeight);if(r){var u=i.x||0,h=i.y||0,c=i.width,d=i.height,f=i.sWidth,p=i.sHeight,g=i.sx||0,m=i.sy||0,v=f&&p,y=this._vmlEl;y||(y=PS.createElement("div"),HS(y),this._vmlEl=y);var x,_=y.style,w=!1,b=1,M=1;if(this.transform&&(x=this.transform,b=zS(x[0]*x[0]+x[1]*x[1]),M=zS(x[2]*x[2]+x[3]*x[3]),w=x[1]||x[2]),w){var S=[u,h],I=[u+c,h],C=[u,h+d],T=[u+c,h+d];$(S,S,x),$(I,I,x),$(C,C,x),$(T,T,x);var D=BS(S[0],I[0],C[0],T[0]),A=BS(S[1],I[1],C[1],T[1]),k=[];k.push("M11=",x[0]/b,",","M12=",x[2]/M,",","M21=",x[1]/b,",","M22=",x[3]/M,",","Dx=",OS(u*b+x[4]),",","Dy=",OS(h*M+x[5])),_.padding="0 "+OS(D)+"px "+OS(A)+"px 0",_.filter="progid:DXImageTransform.Microsoft.Matrix("+k.join("")+", SizingMethod=clip)"}else x&&(u=u*b+x[4],h=h*M+x[5]),_.filter="",_.left=OS(u)+"px",_.top=OS(h)+"px";var P=this._imageEl,L=this._cropEl;P||(P=PS.createElement("div"),this._imageEl=P);var O=P.style;if(v){if(e&&n)O.width=OS(b*e*c/f)+"px",O.height=OS(M*n*d/p)+"px";else{var z=new Image,E=this;z.onload=function(){z.onload=null,e=z.width,n=z.height,O.width=OS(b*e*c/f)+"px",O.height=OS(M*n*d/p)+"px",E._imageWidth=e,E._imageHeight=n,E._imageSrc=r},z.src=r}L||((L=PS.createElement("div")).style.overflow="hidden",this._cropEl=L);var N=L.style;N.width=OS((c+g*c/f)*b),N.height=OS((d+m*d/p)*M),N.filter="progid:DXImageTransform.Microsoft.Matrix(Dx="+-g*c/f*b+",Dy="+-m*d/p*M+")",L.parentNode||y.appendChild(L),P.parentNode!=L&&L.appendChild(P)}else O.width=OS(b*c)+"px",O.height=OS(M*d)+"px",y.appendChild(P),L&&L.parentNode&&(y.removeChild(L),this._cropEl=null);var R="",B=i.opacity;B<1&&(R+=".Alpha(opacity="+OS(100*B)+") "),R+="progid:DXImageTransform.Microsoft.AlphaImageLoader(src="+r+", SizingMethod=scale)",O.filter=R,y.style.zIndex=XS(this.zlevel,this.z,this.z2),ZS(t,y),null!=i.text&&this.drawRectText(t,this.getBoundingRect())}},je.prototype.onRemove=function(t){US(t,this._vmlEl),this._vmlEl=null,this._cropEl=null,this._imageEl=null,this.removeRectText(t)},je.prototype.onAdd=function(t){ZS(t,this._vmlEl),this.appendRectText(t)};var nI,iI={},rI=0,oI=document.createElement("div"),aI=function(t){var e=iI[t];if(!e){rI>100&&(rI=0,iI={});var n,i=oI.style;try{i.font=t,n=i.fontFamily.split(",")[0]}catch(t){}e={style:i.fontStyle||"normal",variant:i.fontVariant||"normal",weight:i.fontWeight||"normal",size:0|parseFloat(i.fontSize||12),family:n||"Microsoft YaHei"},iI[t]=e,rI++}return e};!function(t,e){Zg[t]=e}("measureText",function(t,e){var n=PS;nI||((nI=n.createElement("div")).style.cssText="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;",PS.body.appendChild(nI));try{nI.style.font=e}catch(t){}return nI.innerHTML="",nI.appendChild(n.createTextNode(t)),{width:nI.offsetWidth}});for(var sI=new Xt,lI=[Yg,Xe,je,xi,kv],uI=0;uI<lI.length;uI++){var hI=lI[uI].prototype;hI.drawRectText=function(t,e,n,i){var r=this.style;this.__dirty&&De(r);var o=r.text;if(null!=o&&(o+=""),o){if(r.rich){var a=Se(o,r);o=[];for(var s=0;s<a.lines.length;s++){for(var l=a.lines[s].tokens,u=[],h=0;h<l.length;h++)u.push(l[h].text);o.push(u.join(""))}o=o.join("\n")}var c,d,f=r.textAlign,p=r.textVerticalAlign,g=aI(r.font),m=g.style+" "+g.variant+" "+g.weight+" "+g.size+'px "'+g.family+'"';n=n||ce(o,m,f,p);var v=this.transform;if(v&&!i&&(sI.copy(e),sI.applyTransform(v),e=sI),i)c=e.x,d=e.y;else{var y=r.textPosition,x=r.textDistance;if(y instanceof Array)c=e.x+jS(y[0],e.width),d=e.y+jS(y[1],e.height),f=f||"left";else{var _=me(y,e,x);c=_.x,d=_.y,f=f||_.textAlign,p=p||_.textVerticalAlign}}c=pe(c,n.width,f),d=ge(d,n.height,p),d+=n.height/2;var w,b,M,S=Ff,I=this._textVmlEl;I?b=(w=(M=I.firstChild).nextSibling).nextSibling:(I=S("line"),w=S("path"),b=S("textpath"),M=S("skew"),b.style["v-text-align"]="left",HS(I),w.textpathok=!0,b.on=!0,I.from="0 0",I.to="1000 0.05",ZS(I,M),ZS(I,w),ZS(I,b),this._textVmlEl=I);var C=[c,d],T=I.style;v&&i?($(C,C,v),M.on=!0,M.matrix=v[0].toFixed(3)+","+v[2].toFixed(3)+","+v[1].toFixed(3)+","+v[3].toFixed(3)+",0,0",M.offset=(OS(C[0])||0)+","+(OS(C[1])||0),M.origin="0 0",T.left="0px",T.top="0px"):(M.on=!1,T.left=OS(c)+"px",T.top=OS(d)+"px"),b.string=GS(o);try{b.style.font=m}catch(t){}QS(I,"fill",{fill:r.textFill,opacity:r.opacity},this),QS(I,"stroke",{stroke:r.textStroke,opacity:r.opacity,lineDash:r.lineDash},this),I.style.zIndex=XS(this.zlevel,this.z,this.z2),ZS(t,I)}},hI.removeRectText=function(t){US(t,this._textVmlEl),this._textVmlEl=null},hI.appendRectText=function(t){ZS(t,this._textVmlEl)}}kv.prototype.brushVML=function(t){var e=this.style;null!=e.text?this.drawRectText(t,{x:e.x||0,y:e.y||0,width:0,height:0},this.getBoundingRect(),!0):this.removeRectText(t)},kv.prototype.onRemove=function(t){this.removeRectText(t)},kv.prototype.onAdd=function(t){this.appendRectText(t)}}Wf.prototype={constructor:Wf,getType:function(){return"vml"},getViewportRoot:function(){return this._vmlViewport},getViewportRootOffset:function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},refresh:function(){var t=this.storage.getDisplayList(!0,!0);this._paintList(t)},_paintList:function(t){for(var e=this._vmlRoot,n=0;n<t.length;n++){var i=t[n];i.invisible||i.ignore?(i.__alreadyNotVisible||i.onRemove(e),i.__alreadyNotVisible=!0):(i.__alreadyNotVisible&&i.onAdd(e),i.__alreadyNotVisible=!1,i.__dirty&&(i.beforeBrush&&i.beforeBrush(),(i.brushVML||i.brush).call(i,e),i.afterBrush&&i.afterBrush())),i.__dirty=!1}this._firstPaint&&(this._vmlViewport.appendChild(e),this._firstPaint=!1)},resize:function(t,e){var t=null==t?this._getWidth():t,e=null==e?this._getHeight():e;if(this._width!=t||this._height!=e){this._width=t,this._height=e;var n=this._vmlViewport.style;n.width=t+"px",n.height=e+"px"}},dispose:function(){this.root.innerHTML="",this._vmlRoot=this._vmlViewport=this.storage=null},getWidth:function(){return this._width},getHeight:function(){return this._height},clear:function(){this._vmlViewport&&this.root.removeChild(this._vmlViewport)},_getWidth:function(){var t=this.root,e=t.currentStyle;return(t.clientWidth||Gf(e.width))-Gf(e.paddingLeft)-Gf(e.paddingRight)|0},_getHeight:function(){var t=this.root,e=t.currentStyle;return(t.clientHeight||Gf(e.height))-Gf(e.paddingTop)-Gf(e.paddingBottom)|0}},d(["getLayer","insertLayer","eachLayer","eachBuiltinLayer","eachOtherLayer","getLayers","modLayer","delLayer","clearLayer","toDataURL","pathToImage"],function(t){Wf.prototype[t]=Zf(t)}),vn("vml",Wf);var cI="http://www.w3.org/2000/svg",dI=av.CMD,fI=Array.prototype.join,pI="none",gI=Math.round,mI=Math.sin,vI=Math.cos,yI=Math.PI,xI=2*Math.PI,_I=180/yI,wI=1e-4,bI={};bI.brush=function(t){var e=t.style,n=t.__svgEl;n||(n=Uf("path"),t.__svgEl=n),t.path||t.createPathProxy();var i=t.path;if(t.__dirtyPath){i.beginPath(),t.buildPath(i,t.shape),t.__dirtyPath=!1;var r=tp(i);r.indexOf("NaN")<0&&Kf(n,"d",r)}Jf(n,e),$f(n,t.transform),null!=e.text&&CI(t,t.getBoundingRect())};var MI={};MI.brush=function(t){var e=t.style,n=e.image;if(n instanceof HTMLImageElement&&(n=n.src),n){var i=e.x||0,r=e.y||0,o=e.width,a=e.height,s=t.__svgEl;s||(s=Uf("image"),t.__svgEl=s),n!==t.__imageSrc&&(Qf(s,"href",n),t.__imageSrc=n),Kf(s,"width",o),Kf(s,"height",a),Kf(s,"x",i),Kf(s,"y",r),$f(s,t.transform),null!=e.text&&CI(t,t.getBoundingRect())}};var SI={},II=new Xt,CI=function(t,e,n){var i=t.style;t.__dirty&&De(i);var r=i.text;if(null!=r){r+="";var o=t.__textSvgEl;o||(o=Uf("text"),t.__textSvgEl=o);var a,s,l=i.textPosition,u=i.textDistance,h=i.textAlign||"left";"number"==typeof i.fontSize&&(i.fontSize+="px");var c=i.font||[i.fontStyle||"",i.fontWeight||"",i.fontSize||"",i.fontFamily||""].join(" ")||Wg,d=ep(i.textVerticalAlign),f=(n=ce(r,c,h,d)).lineHeight;if(l instanceof Array)a=e.x+l[0],s=e.y+l[1];else{var p=me(l,e,u);a=p.x,s=p.y,d=ep(p.textVerticalAlign),h=p.textAlign}Kf(o,"alignment-baseline",d),c&&(o.style.font=c);var g=i.textPadding;if(Kf(o,"x",a),Kf(o,"y",s),Jf(o,i,!0),t instanceof kv||t.style.transformText)$f(o,t.transform);else{if(t.transform)II.copy(e),II.applyTransform(t.transform),e=II;else{var m=t.transformCoordToGlobal(e.x,e.y);e.x=m[0],e.y=m[1]}var v=i.textOrigin;"center"===v?(a=n.width/2+a,s=n.height/2+s):v&&(a=v[0]+a,s=v[1]+s);var y=-i.textRotation||0,x=rt();ut(x,t.transform,y),$f(o,x)}var _=r.split("\n"),w=_.length,b=h;"left"===b?(b="start",g&&(a+=g[3])):"right"===b?(b="end",g&&(a-=g[1])):"center"===b&&(b="middle",g&&(a+=(g[3]-g[1])/2));var M=0;if("baseline"===d?(M=-n.height+f,g&&(M-=g[2])):"middle"===d?(M=(-n.height+f)/2,g&&(s+=(g[0]-g[2])/2)):g&&(M+=g[0]),t.__text!==r||t.__textFont!==c){var S=t.__tspanList||[];t.__tspanList=S;for(C=0;C<w;C++)(T=S[C])?T.innerHTML="":(T=S[C]=Uf("tspan"),o.appendChild(T),Kf(T,"alignment-baseline",d),Kf(T,"text-anchor",b)),Kf(T,"x",a),Kf(T,"y",s+C*f+M),T.appendChild(document.createTextNode(_[C]));for(;C<S.length;C++)o.removeChild(S[C]);S.length=w,t.__text=r,t.__textFont=c}else if(t.__tspanList.length)for(var I=t.__tspanList.length,C=0;C<I;++C){var T=t.__tspanList[C];T&&(Kf(T,"x",a),Kf(T,"y",s+C*f+M))}}};SI.drawRectText=CI,SI.brush=function(t){var e=t.style;null!=e.text&&(e.textPosition=[0,0],CI(t,{x:e.x||0,y:e.y||0,width:0,height:0},t.getBoundingRect()))},np.prototype={diff:function(t,e,n){n||(n=function(t,e){return t===e}),this.equals=n;var i=this;t=t.slice();var r=(e=e.slice()).length,o=t.length,a=1,s=r+o,l=[{newPos:-1,components:[]}],u=this.extractCommon(l[0],e,t,0);if(l[0].newPos+1>=r&&u+1>=o){for(var h=[],c=0;c<e.length;c++)h.push(c);return[{indices:h,count:e.length}]}for(;a<=s;){var d=function(){for(var n=-1*a;n<=a;n+=2){var s,u=l[n-1],h=l[n+1],c=(h?h.newPos:0)-n;u&&(l[n-1]=void 0);var d=u&&u.newPos+1<r,f=h&&0<=c&&c<o;if(d||f){if(!d||f&&u.newPos<h.newPos?(s=rp(h),i.pushComponent(s.components,void 0,!0)):((s=u).newPos++,i.pushComponent(s.components,!0,void 0)),c=i.extractCommon(s,e,t,n),s.newPos+1>=r&&c+1>=o)return ip(0,s.components);l[n]=s}else l[n]=void 0}a++}();if(d)return d}},pushComponent:function(t,e,n){var i=t[t.length-1];i&&i.added===e&&i.removed===n?t[t.length-1]={count:i.count+1,added:e,removed:n}:t.push({count:1,added:e,removed:n})},extractCommon:function(t,e,n,i){for(var r=e.length,o=n.length,a=t.newPos,s=a-i,l=0;a+1<r&&s+1<o&&this.equals(e[a+1],n[s+1]);)a++,s++,l++;return l&&t.components.push({count:l}),t.newPos=a,s},tokenize:function(t){return t.slice()},join:function(t){return t.slice()}};var TI=new np,DI=function(t,e,n){return TI.diff(t,e,n)};op.prototype.createElement=Uf,op.prototype.getDefs=function(t){var e=this._svgRoot,n=this._svgRoot.getElementsByTagName("defs");return 0===n.length?t?((n=e.insertBefore(this.createElement("defs"),e.firstChild)).contains||(n.contains=function(t){var e=n.children;if(!e)return!1;for(var i=e.length-1;i>=0;--i)if(e[i]===t)return!0;return!1}),n):null:n[0]},op.prototype.update=function(t,e){if(t){var n=this.getDefs(!1);if(t[this._domName]&&n.contains(t[this._domName]))"function"==typeof e&&e(t);else{var i=this.add(t);i&&(t[this._domName]=i)}}},op.prototype.addDom=function(t){this.getDefs(!0).appendChild(t)},op.prototype.removeDom=function(t){var e=this.getDefs(!1);e&&t[this._domName]&&(e.removeChild(t[this._domName]),t[this._domName]=null)},op.prototype.getDoms=function(){var t=this.getDefs(!1);if(!t)return[];var e=[];return d(this._tagNames,function(n){var i=t.getElementsByTagName(n);e=e.concat([].slice.call(i))}),e},op.prototype.markAllUnused=function(){var t=this;d(this.getDoms(),function(e){e[t._markLabel]="0"})},op.prototype.markUsed=function(t){t&&(t[this._markLabel]="1")},op.prototype.removeUnused=function(){var t=this.getDefs(!1);if(t){var e=this;d(this.getDoms(),function(n){"1"!==n[e._markLabel]&&t.removeChild(n)})}},op.prototype.getSvgProxy=function(t){return t instanceof xi?bI:t instanceof je?MI:t instanceof kv?SI:bI},op.prototype.getTextSvgElement=function(t){return t.__textSvgEl},op.prototype.getSvgElement=function(t){return t.__svgEl},u(ap,op),ap.prototype.addWithoutUpdate=function(t,e){if(e&&e.style){var n=this;d(["fill","stroke"],function(i){if(e.style[i]&&("linear"===e.style[i].type||"radial"===e.style[i].type)){var r,o=e.style[i],a=n.getDefs(!0);o._dom?(r=o._dom,a.contains(o._dom)||n.addDom(r)):r=n.add(o),n.markUsed(e);var s=r.getAttribute("id");t.setAttribute(i,"url(#"+s+")")}})}},ap.prototype.add=function(t){var e;if("linear"===t.type)e=this.createElement("linearGradient");else{if("radial"!==t.type)return yg("Illegal gradient type."),null;e=this.createElement("radialGradient")}return t.id=t.id||this.nextId++,e.setAttribute("id","zr"+this._zrId+"-gradient-"+t.id),this.updateDom(t,e),this.addDom(e),e},ap.prototype.update=function(t){var e=this;op.prototype.update.call(this,t,function(){var n=t.type,i=t._dom.tagName;"linear"===n&&"linearGradient"===i||"radial"===n&&"radialGradient"===i?e.updateDom(t,t._dom):(e.removeDom(t),e.add(t))})},ap.prototype.updateDom=function(t,e){if("linear"===t.type)e.setAttribute("x1",t.x),e.setAttribute("y1",t.y),e.setAttribute("x2",t.x2),e.setAttribute("y2",t.y2);else{if("radial"!==t.type)return void yg("Illegal gradient type.");e.setAttribute("cx",t.x),e.setAttribute("cy",t.y),e.setAttribute("r",t.r)}t.global?e.setAttribute("gradientUnits","userSpaceOnUse"):e.setAttribute("gradientUnits","objectBoundingBox"),e.innerHTML="";for(var n=t.colorStops,i=0,r=n.length;i<r;++i){var o=this.createElement("stop");o.setAttribute("offset",100*n[i].offset+"%"),o.setAttribute("stop-color",n[i].color),e.appendChild(o)}t._dom=e},ap.prototype.markUsed=function(t){if(t.style){var e=t.style.fill;e&&e._dom&&op.prototype.markUsed.call(this,e._dom),(e=t.style.stroke)&&e._dom&&op.prototype.markUsed.call(this,e._dom)}},u(sp,op),sp.prototype.update=function(t){var e=this.getSvgElement(t);e&&this.updateDom(e,t.__clipPaths,!1);var n=this.getTextSvgElement(t);n&&this.updateDom(n,t.__clipPaths,!0),this.markUsed(t)},sp.prototype.updateDom=function(t,e,n){if(e&&e.length>0){var i,r,o=this.getDefs(!0),a=e[0],s=n?"_textDom":"_dom";a[s]?(r=a[s].getAttribute("id"),i=a[s],o.contains(i)||o.appendChild(i)):(r="zr"+this._zrId+"-clip-"+this.nextId,++this.nextId,(i=this.createElement("clipPath")).setAttribute("id",r),o.appendChild(i),a[s]=i);var l=this.getSvgProxy(a);if(a.transform&&a.parent.invTransform&&!n){var u=Array.prototype.slice.call(a.transform);st(a.transform,a.parent.invTransform,a.transform),l.brush(a),a.transform=u}else l.brush(a);var h=this.getSvgElement(a);i.innerHTML="",i.appendChild(h.cloneNode()),t.setAttribute("clip-path","url(#"+r+")"),e.length>1&&this.updateDom(i,e.slice(1),n)}else t&&t.setAttribute("clip-path","none")},sp.prototype.markUsed=function(t){var e=this;t.__clipPaths&&t.__clipPaths.length>0&&d(t.__clipPaths,function(t){t._dom&&op.prototype.markUsed.call(e,t._dom),t._textDom&&op.prototype.markUsed.call(e,t._textDom)})},u(lp,op),lp.prototype.addWithoutUpdate=function(t,e){if(e&&up(e.style)){var n,i=e.style;i._shadowDom?(n=i._shadowDom,this.getDefs(!0).contains(i._shadowDom)||this.addDom(n)):n=this.add(e),this.markUsed(e);var r=n.getAttribute("id");t.style.filter="url(#"+r+")"}},lp.prototype.add=function(t){var e=this.createElement("filter"),n=t.style;return n._shadowDomId=n._shadowDomId||this.nextId++,e.setAttribute("id","zr"+this._zrId+"-shadow-"+n._shadowDomId),this.updateDom(t,e),this.addDom(e),e},lp.prototype.update=function(t,e){var n=e.style;if(up(n)){var i=this;op.prototype.update.call(this,e,function(t){i.updateDom(e,t._shadowDom)})}else this.remove(t,n)},lp.prototype.remove=function(t,e){null!=e._shadowDomId&&(this.removeDom(e),t.style.filter="")},lp.prototype.updateDom=function(t,e){var n=e.getElementsByTagName("feDropShadow");n=0===n.length?this.createElement("feDropShadow"):n[0];var i,r,o,a,s=t.style,l=t.scale?t.scale[0]||1:1,u=t.scale?t.scale[1]||1:1;if(s.shadowBlur||s.shadowOffsetX||s.shadowOffsetY)i=s.shadowOffsetX||0,r=s.shadowOffsetY||0,o=s.shadowBlur,a=s.shadowColor;else{if(!s.textShadowBlur)return void this.removeDom(e,s);i=s.textShadowOffsetX||0,r=s.textShadowOffsetY||0,o=s.textShadowBlur,a=s.textShadowColor}n.setAttribute("dx",i/l),n.setAttribute("dy",r/u),n.setAttribute("flood-color",a);var h=o/2/l+" "+o/2/u;n.setAttribute("stdDeviation",h),e.setAttribute("x","-100%"),e.setAttribute("y","-100%"),e.setAttribute("width",Math.ceil(o/2*200)+"%"),e.setAttribute("height",Math.ceil(o/2*200)+"%"),e.appendChild(n),s._shadowDom=e},lp.prototype.markUsed=function(t){var e=t.style;e&&e._shadowDom&&op.prototype.markUsed.call(this,e._shadowDom)};var AI=function(t,e,n,i){this.root=t,this.storage=e,this._opts=n=o({},n||{});var r=Uf("svg");r.setAttribute("xmlns","http://www.w3.org/2000/svg"),r.setAttribute("version","1.1"),r.setAttribute("baseProfile","full"),r.style.cssText="user-select:none;position:absolute;left:0;top:0;",this.gradientManager=new ap(i,r),this.clipPathManager=new sp(i,r),this.shadowManager=new lp(i,r);var a=document.createElement("div");a.style.cssText="overflow:hidden;position:relative",this._svgRoot=r,this._viewport=a,t.appendChild(a),a.appendChild(r),this.resize(n.width,n.height),this._visibleList=[]};AI.prototype={constructor:AI,getType:function(){return"svg"},getViewportRoot:function(){return this._viewport},getViewportRootOffset:function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},refresh:function(){var t=this.storage.getDisplayList(!0);this._paintList(t)},setBackgroundColor:function(t){this._viewport.style.background=t},_paintList:function(t){this.gradientManager.markAllUnused(),this.clipPathManager.markAllUnused(),this.shadowManager.markAllUnused();var e,n=this._svgRoot,i=this._visibleList,r=t.length,o=[];for(e=0;e<r;e++){var a=cp(f=t[e]),s=vp(f)||mp(f);f.invisible||(f.__dirty&&(a&&a.brush(f),this.clipPathManager.update(f),f.style&&(this.gradientManager.update(f.style.fill),this.gradientManager.update(f.style.stroke),this.shadowManager.update(s,f)),f.__dirty=!1),o.push(f))}var l,u=DI(i,o);for(e=0;e<u.length;e++)if((c=u[e]).removed)for(d=0;d<c.count;d++){var s=vp(f=i[c.indices[d]]),h=mp(f);gp(n,s),gp(n,h)}for(e=0;e<u.length;e++){var c=u[e];if(c.added)for(d=0;d<c.count;d++){var s=vp(f=o[c.indices[d]]),h=mp(f);l?fp(n,s,l):pp(n,s),s?fp(n,h,s):l?fp(n,h,l):pp(n,h),fp(n,h,s),l=h||s||l,this.gradientManager.addWithoutUpdate(s,f),this.shadowManager.addWithoutUpdate(l,f),this.clipPathManager.markUsed(f)}else if(!c.removed)for(var d=0;d<c.count;d++){var f=o[c.indices[d]];l=s=mp(f)||vp(f)||l,this.gradientManager.markUsed(f),this.gradientManager.addWithoutUpdate(s,f),this.shadowManager.markUsed(f),this.shadowManager.addWithoutUpdate(s,f),this.clipPathManager.markUsed(f)}}this.gradientManager.removeUnused(),this.clipPathManager.removeUnused(),this.shadowManager.removeUnused(),this._visibleList=o},_getDefs:function(t){var e=this._svgRoot,n=this._svgRoot.getElementsByTagName("defs");return 0===n.length?t?((n=e.insertBefore(Uf("defs"),e.firstChild)).contains||(n.contains=function(t){var e=n.children;if(!e)return!1;for(var i=e.length-1;i>=0;--i)if(e[i]===t)return!0;return!1}),n):null:n[0]},resize:function(t,e){var n=this._viewport;n.style.display="none";var i=this._opts;if(null!=t&&(i.width=t),null!=e&&(i.height=e),t=this._getSize(0),e=this._getSize(1),n.style.display="",this._width!==t||this._height!==e){this._width=t,this._height=e;var r=n.style;r.width=t+"px",r.height=e+"px";var o=this._svgRoot;o.setAttribute("width",t),o.setAttribute("height",e)}},getWidth:function(){return this._width},getHeight:function(){return this._height},_getSize:function(t){var e=this._opts,n=["width","height"][t],i=["clientWidth","clientHeight"][t],r=["paddingLeft","paddingTop"][t],o=["paddingRight","paddingBottom"][t];if(null!=e[n]&&"auto"!==e[n])return parseFloat(e[n]);var a=this.root,s=document.defaultView.getComputedStyle(a);return(a[i]||hp(s[n])||hp(a.style[n]))-(hp(s[r])||0)-(hp(s[o])||0)|0},dispose:function(){this.root.innerHTML="",this._svgRoot=this._viewport=this.storage=null},clear:function(){this._viewport&&this.root.removeChild(this._viewport)},pathToDataUrl:function(){return this.refresh(),"data:image/svg+xml;charset=UTF-8,"+this._svgRoot.outerHTML}},d(["getLayer","insertLayer","eachLayer","eachBuiltinLayer","eachOtherLayer","getLayers","modLayer","delLayer","clearLayer","toDataURL","pathToImage"],function(t){AI.prototype[t]=yp(t)}),vn("svg",AI),t.version="4.1.0",t.dependencies=Gx,t.PRIORITY=Xx,t.init=function(t,e,n){var i=$a(t);if(i)return i;var r=new Aa(t,e,n);return r.id="ec_"+u_++,s_[r.id]=r,Pn(t,c_,r.id),Ya(r),r},t.connect=function(t){if(y(t)){var e=t;t=null,Bx(e,function(e){null!=e.group&&(t=e.group)}),t=t||"g_"+h_++,Bx(e,function(e){e.group=t})}return l_[t]=!0,t},t.disConnect=qa,t.disconnect=f_,t.dispose=function(t){"string"==typeof t?t=s_[t]:t instanceof Aa||(t=$a(t)),t instanceof Aa&&!t.isDisposed()&&t.dispose()},t.getInstanceByDom=$a,t.getInstanceById=function(t){return s_[t]},t.registerTheme=Ka,t.registerPreprocessor=Qa,t.registerProcessor=Ja,t.registerPostUpdate=function(t){i_.push(t)},t.registerAction=ts,t.registerCoordinateSystem=function(t,e){yo.register(t,e)},t.getCoordinateSystemDimensions=function(t){var e=yo.get(t);if(e)return e.getDimensionsInfo?e.getDimensionsInfo():e.dimensions.slice()},t.registerLayout=es,t.registerVisual=ns,t.registerLoading=rs,t.extendComponentModel=os,t.extendComponentView=as,t.extendSeriesModel=ss,t.extendChartView=ls,t.setCanvasCreator=function(t){e("createCanvas",t)},t.registerMap=function(t,e,n){e.geoJson&&!e.features&&(n=e.specialAreas,e=e.geoJson),"string"==typeof e&&(e="undefined"!=typeof JSON&&JSON.parse?JSON.parse(e):new Function("return ("+e+");")()),d_[t]={geoJson:e,specialAreas:n}},t.getMap=function(t){return d_[t]},t.dataTool=p_,t.zrender=pm,t.graphic=ty,t.number=hy,t.format=yy,t.throttle=ua,t.helper=aw,t.matrix=qp,t.vector=Gp,t.color=dg,t.parseGeoJSON=lw,t.parseGeoJson=dw,t.util=fw,t.List=M_,t.Model=pr,t.Axis=cw,t.env=bp}); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js new file mode 100644 index 0000000..c7f3a59 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js @@ -0,0 +1,117 @@ +/*! + * jQuery Cookie Plugin v1.4.1 + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2013 Klaus Hartl + * Released under the MIT license + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + + var pluses = /\+/g; + + function encode(s) { + return config.raw ? s : encodeURIComponent(s); + } + + function decode(s) { + return config.raw ? s : decodeURIComponent(s); + } + + function stringifyCookieValue(value) { + return encode(config.json ? JSON.stringify(value) : String(value)); + } + + function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); + return config.json ? JSON.parse(s) : s; + } catch(e) {} + } + + function read(s, converter) { + var value = config.raw ? s : parseCookieValue(s); + return $.isFunction(converter) ? converter(value) : value; + } + + var config = $.cookie = function (key, value, options) { + + // Write + + if (value !== undefined && !$.isFunction(value)) { + options = $.extend({}, config.defaults, options); + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setTime(+t + days * 864e+5); + } + + return (document.cookie = [ + encode(key), '=', stringifyCookieValue(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // Read + + var result = key ? undefined : {}; + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling $.cookie(). + var cookies = document.cookie ? document.cookie.split('; ') : []; + + for (var i = 0, l = cookies.length; i < l; i++) { + var parts = cookies[i].split('='); + var name = decode(parts.shift()); + var cookie = parts.join('='); + + if (key && key === name) { + // If second argument (value) is a function it's a converter... + result = read(cookie, value); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (cookie = read(cookie)) !== undefined) { + result[name] = cookie; + } + } + + return result; + }; + + config.defaults = {}; + + $.removeCookie = function (key, options) { + if ($.cookie(key) === undefined) { + return false; + } + + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return !$.cookie(key); + }; + +})); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js new file mode 100644 index 0000000..d959022 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js @@ -0,0 +1,4 @@ +/*! jQuery Validation Plugin - v1.18.0 - 9/9/2018 + * https://jqueryvalidation.org/ + * Copyright (c) 2018 Jörn Zaefferer; Licensed MIT */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.submitButton=b.currentTarget,a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.submitButton&&(c.settings.submitHandler||c.formSubmitted)&&(d=a("<input type='hidden'/>").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),!(c.settings.submitHandler&&!c.settings.debug)||(e=c.settings.submitHandler.call(c,c.currentForm,b),d&&d.remove(),void 0!==e&&e)}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0];if(null!=j&&(!j.form&&j.isContentEditable&&(j.form=this.closest("form")[0],j.name=this.attr("name")),null!=j.form)){if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(a,b){i[b]=f[b],delete f[b]}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g)),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}}),a.extend(a.expr.pseudos||a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){var c=a(b).val();return null!==c&&!!a.trim(""+c)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||a.inArray(c.keyCode,d)!==-1||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){if(!this.form&&this.isContentEditable&&(this.form=a(this).closest("form")[0],this.name=a(this).attr("name")),d===this.form){var c=a.data(this.form,"validator"),e="on"+b.type.replace(/^validate/,""),f=c.settings;f[e]&&!a(this).is(f.ignore)&&f[e].call(c,this,b)}}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.currentForm,e=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){e[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable], [type='button']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=g.check(e)&&h))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)void 0!==a[b]&&null!==a[b]&&a[b]!==!1&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.isContentEditable&&(this.form=a(this).closest("form")[0],this.name=d),this.form===b.currentForm&&(!(d in c||!b.objectLength(a(this).rules()))&&(c[d]=!0,!0))})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type;return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=b.isContentEditable?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f,g=a(b).rules(),h=a.map(g,function(a,b){return b}).length,i=!1,j=this.elementValue(b);"function"==typeof g.normalizer?f=g.normalizer:"function"==typeof this.settings.normalizer&&(f=this.settings.normalizer),f&&(j=f.call(b,j),delete g.normalizer);for(d in g){e={method:d,parameters:g[d]};try{if(c=a.validator.methods[d].call(this,j,b,e.parameters),"dependency-mismatch"===c&&1===h){i=!0;continue}if(i=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(k){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",k),k instanceof TypeError&&(k.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),k}}if(!i)return this.objectLength(g)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;a<arguments.length;a++)if(void 0!==arguments[a])return arguments[a]},defaultMessage:function(b,c){"string"==typeof c&&(c={method:c});var d=this.findDefined(this.customMessage(b.name,c.method),this.customDataMessage(b,c.method),!this.settings.ignoreTitle&&b.title||void 0,a.validator.messages[c.method],"<strong>Warning: No message defined for "+b.name+"</strong>"),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement.call(this,d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,.\/:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return!this.dependTypes[typeof a]||this.dependTypes[typeof a](a,b)},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.submitButton&&a("input:hidden[name='"+this.submitButton.name+"']",this.currentForm).remove(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return c="string"==typeof c&&c||"remote",a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur").find(".validate-lessThan-blur").off(".validate-lessThan").removeClass("validate-lessThan-blur").find(".validate-lessThanEqual-blur").off(".validate-lessThanEqual").removeClass("validate-lessThanEqual-blur").find(".validate-greaterThanEqual-blur").off(".validate-greaterThanEqual").removeClass("validate-greaterThanEqual-blur").find(".validate-greaterThan-blur").off(".validate-greaterThan").removeClass("validate-greaterThan-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),""===d&&(d=!0),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0===e.param||e.param:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(d,e){b[d]=a.isFunction(e)&&"normalizer"!==d?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:void 0!==b&&null!==b&&b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[\/?#]\S*)?$/i.test(a)},date:function(){var a=!1;return function(b,c){return a||(a=!0,this.settings.debug&&window.console&&console.warn("The `date` method is deprecated and will be removed in version '2.0.0'.\nPlease don't use it, since it relies on the Date constructor, which\nbehaves very differently across browsers and locales. Use `dateISO`\ninstead or one of the locale specific methods in `localizations/`\nand `additional-methods.js`.")),this.optional(c)||!/Invalid|NaN/.test(new Date(b).toString())}}(),dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e<=d},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e,f=a(c).attr("type"),g="Step attribute on input type "+f+" is not supported.",h=["text","number","range"],i=new RegExp("\\b"+f+"\\b"),j=f&&!i.test(h.join()),k=function(a){var b=(""+a).match(/(?:\.(\d+))?$/);return b&&b[1]?b[1].length:0},l=function(a){return Math.round(a*Math.pow(10,e))},m=!0;if(j)throw new Error(g);return e=k(d),(k(b)>e||l(b)%l(d)!==0)&&(m=!1),this.optional(c)||m},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var b,c={};return a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)}),a}); \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js new file mode 100644 index 0000000..12cb6b5 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js @@ -0,0 +1,2 @@ +/*! layer-v3.1.1 Web弹层组件 MIT License http://layer.layui.com/ By 贤心 */ + ;!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'<div class="layui-layer-title" style="'+(f?r.title[1]:"")+'">'+(f?r.title[0]:r.title)+"</div>":"";return r.zIndex=s,t([r.shade?'<div class="layui-layer-shade" id="layui-layer-shade'+a+'" times="'+a+'" style="'+("z-index:"+(s-1)+"; ")+'"></div>':"",'<div class="'+l[0]+(" layui-layer-"+o.type[r.type])+(0!=r.type&&2!=r.type||r.shade?"":" layui-layer-border")+" "+(r.skin||"")+'" id="'+l[0]+a+'" type="'+o.type[r.type]+'" times="'+a+'" showtime="'+r.time+'" conType="'+(e?"object":"string")+'" style="z-index: '+s+"; width:"+r.area[0]+";height:"+r.area[1]+(r.fixed?"":";position:absolute;")+'">'+(e&&2!=r.type?"":u)+'<div id="'+(r.id||"")+'" class="layui-layer-content'+(0==r.type&&r.icon!==-1?" layui-layer-padding":"")+(3==r.type?" layui-layer-loading"+r.icon:"")+'">'+(0==r.type&&r.icon!==-1?'<i class="layui-layer-ico layui-layer-ico'+r.icon+'"></i>':"")+(1==r.type&&e?"":r.content||"")+'</div><span class="layui-layer-setwin">'+function(){var e=c?'<a class="layui-layer-min" href="javascript:;"><cite></cite></a><a class="layui-layer-ico layui-layer-max" href="javascript:;"></a>':"";return r.closeBtn&&(e+='<a class="layui-layer-ico '+l[7]+" "+l[7]+(r.title?r.closeBtn:4==r.type?"1":"2")+'" href="javascript:;"></a>'),e}()+"</span>"+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t<i;t++)e+='<a class="'+l[6]+t+'">'+r.btn[t]+"</a>";return'<div class="'+l[6]+" layui-layer-btn-"+(r.btnAlign||"")+'">'+e+"</div>"}():"")+(r.resize?'<span class="layui-layer-resize"></span>':"")+"</div>"],u,i('<div class="layui-layer-move"></div>')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='<iframe scrolling="'+(t.content[1]||"auto")+'" allowtransparency="true" id="'+l[4]+a+'" name="'+l[4]+a+'" onload="this.className=\'\';" class="layui-layer-load" frameborder="0" src="'+t.content[0]+'"></iframe>';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'<i class="layui-layer-TipsG"></i>',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;a<c.stX&&(a=c.stX),a>f&&(a=f),o<c.stY&&(o=c.stY),o>u&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'<textarea class="layui-layer-input"'+a+">"+(e.value||"")+"</textarea>":function(){return'<input type="'+(1==e.formType?"password":"text")+'" class="layui-layer-input" value="'+(e.value||"")+'">'}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a='<span class="'+n+'">'+t[0].title+"</span>";i<e;i++)a+="<span>"+t[i].title+"</span>";return a}(),content:'<ul class="layui-layer-tabmain">'+function(){var e=t.length,i=1,a="";if(e>0)for(a='<li class="layui-layer-tabli '+n+'">'+(t[0].content||"no content")+"</li>";i<e;i++)a+='<li class="layui-layer-tabli">'+(t[i].content||"no content")+"</li>";return a}()+"</ul>",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]<r[1]&&(a[0]=a[0]/r[1],a[1]=a[1]/r[1])}return[a[0]+"px",a[1]+"px"]}(),title:!1,shade:.9,shadeClose:!0,closeBtn:!1,move:".layui-layer-phimg img",moveType:1,scrollbar:!1,moveOut:!0,isOutAnim:!1,skin:"layui-layer-photos"+c("photos"),content:'<div class="layui-layer-phimg"><img src="'+u[d].src+'" alt="'+(u[d].alt||"")+'" layer-pid="'+u[d].pid+'"><div class="layui-layer-imgsee">'+(u.length>1?'<span class="layui-layer-imguide"><a href="javascript:;" class="layui-layer-iconext layui-layer-imgprev"></a><a href="javascript:;" class="layui-layer-iconext layui-layer-imgnext"></a></span>':"")+'<div class="layui-layer-imgbar" style="display:'+(a?"block":"")+'"><span class="layui-layer-imgtit"><a href="javascript:;">'+(u[d].alt||"")+"</a><em>"+s.imgIndex+"/"+u.length+"</em></span></div></div></div>",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常<br>是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png new file mode 100644 index 0000000..bbbb669 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png new file mode 100644 index 0000000..3e17da8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css new file mode 100644 index 0000000..820b4a9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css @@ -0,0 +1 @@ +.layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1E9FFF;background-color:#1E9FFF;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#E9E7E7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:230px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;overflow:hidden;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:43px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif new file mode 100644 index 0000000..6f3c953 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif new file mode 100644 index 0000000..db3a483 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif new file mode 100644 index 0000000..5bb90fd --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif Binary files differ diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl new file mode 100644 index 0000000..e448125 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Error</title> + <style type="text/css"> + body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } + div.dialog { + width: 80%; + padding: 1em 4em; + margin: 4em auto 0 auto; + border: 1px solid #ccc; + border-right-color: #999; + border-bottom-color: #999; + } + h1 { font-size: 100%; color: #f00; line-height: 1.5em; } + </style> + +</head> +</head> +<body> + + <div class="dialog"> + <h1>System Error</h1> + <p>${exceptionMsg}</p> + <a href="javascript:window.location.href='${request.contextPath}/'">Back</a> + </p> + </div> + +</body> +</html> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl new file mode 100644 index 0000000..aace849 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl @@ -0,0 +1,239 @@ +<#macro commonStyle> + + <#-- favicon --> + <link rel="icon" href="${request.contextPath}/static/favicon.ico" /> + + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <!-- Tell the browser to be responsive to screen width --> + <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> + <!-- Bootstrap --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css"> + <!-- Font Awesome --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/font-awesome/css/font-awesome.min.css"> + <!-- Ionicons --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/Ionicons/css/ionicons.min.css"> + <!-- Theme style --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/dist/css/AdminLTE.min.css"> + <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/dist/css/skins/_all-skins.min.css"> + + <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> + <!--[if lt IE 9]> + <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script> + <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> + <![endif]--> + + <!-- pace --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css"> + + <#-- i18n --> + <#global I18n = I18nUtil.getMultString()?eval /> + +</#macro> + +<#macro commonScript> + <!-- jQuery --> + <script src="${request.contextPath}/static/adminlte/bower_components/jquery/jquery.min.js"></script> + <!-- Bootstrap --> + <script src="${request.contextPath}/static/adminlte/bower_components/bootstrap/js/bootstrap.min.js"></script> + <!-- FastClick --> + <script src="${request.contextPath}/static/adminlte/bower_components/fastclick/fastclick.js"></script> + <!-- AdminLTE App --> + <script src="${request.contextPath}/static/adminlte/dist/js/adminlte.min.js"></script> + <!-- jquery.slimscroll --> + <script src="${request.contextPath}/static/adminlte/bower_components/jquery-slimscroll/jquery.slimscroll.min.js"></script> + + <!-- pace --> + <script src="${request.contextPath}/static/adminlte/bower_components/PACE/pace.min.js"></script> + <#-- jquery cookie --> + <script src="${request.contextPath}/static/plugins/jquery/jquery.cookie.js"></script> + <#-- jquery.validate --> + <script src="${request.contextPath}/static/plugins/jquery/jquery.validate.min.js"></script> + + <#-- layer --> + <script src="${request.contextPath}/static/plugins/layer/layer.js"></script> + + <#-- common --> + <script src="${request.contextPath}/static/js/common.1.js"></script> + <script> + var base_url = '${request.contextPath}'; + var I18n = ${I18nUtil.getMultString()}; + </script> + +</#macro> + +<#macro commonHeader> + <header class="main-header"> + <a href="${request.contextPath}/" class="logo"> + <span class="logo-mini"><b>XXL</b></span> + <span class="logo-lg"><b>${I18n.admin_name}</b></span> + </a> + <nav class="navbar navbar-static-top" role="navigation"> + + <a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </a> + + <div class="navbar-custom-menu"> + <ul class="nav navbar-nav"> + <#-- login user --> + <li class="dropdown"> + <a href="javascript:" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> + ${I18n.system_welcome} ${Request["XXL_JOB_LOGIN_IDENTITY"].username} + <span class="caret"></span> + </a> + <ul class="dropdown-menu" role="menu"> + <li id="updatePwd" ><a href="javascript:">${I18n.change_pwd}</a></li> + <li id="logoutBtn" ><a href="javascript:">${I18n.logout_btn}</a></li> + </ul> + </li> + </ul> + </div> + + </nav> + </header> + + <!-- 修改密码.模态框 --> + <div class="modal fade" id="updatePwdModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog "> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.change_pwd}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.change_pwd_field_newpwd}<font color="red">*</font></label> + <div class="col-sm-10"><input type="text" class="form-control" name="password" placeholder="${I18n.system_please_input} ${I18n.change_pwd_field_newpwd}" maxlength="18" ></div> + </div> + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + +</#macro> + +<#macro commonLeft pageName > + <!-- Left side column. contains the logo and sidebar --> + <aside class="main-sidebar"> + <!-- sidebar: style can be found in sidebar.less --> + <section class="sidebar"> + <!-- sidebar menu: : style can be found in sidebar.less --> + <ul class="sidebar-menu"> + <li class="header">${I18n.system_nav}</li> + <li class="nav-click <#if pageName == "index">active</#if>" ><a href="${request.contextPath}/"><i class="fa fa-circle-o text-aqua"></i><span>${I18n.job_dashboard_name}</span></a></li> + <li class="nav-click <#if pageName == "jobinfo">active</#if>" ><a href="${request.contextPath}/jobinfo"><i class="fa fa-circle-o text-yellow"></i><span>${I18n.jobinfo_name}</span></a></li> + <li class="nav-click <#if pageName == "joblog">active</#if>" ><a href="${request.contextPath}/joblog"><i class="fa fa-circle-o text-green"></i><span>${I18n.joblog_name}</span></a></li> + <#if Request["XXL_JOB_LOGIN_IDENTITY"].role == 1> + <li class="nav-click <#if pageName == "jobgroup">active</#if>" ><a href="${request.contextPath}/jobgroup"><i class="fa fa-circle-o text-red"></i><span>${I18n.jobgroup_name}</span></a></li> + <li class="nav-click <#if pageName == "user">active</#if>" ><a href="${request.contextPath}/user"><i class="fa fa-circle-o text-purple"></i><span>${I18n.user_manage}</span></a></li> + </#if> + <li class="nav-click <#if pageName == "help">active</#if>" ><a href="${request.contextPath}/help"><i class="fa fa-circle-o text-gray"></i><span>${I18n.job_help}</span></a></li> + </ul> + </section> + <!-- /.sidebar --> + </aside> +</#macro> + +<#macro commonControl > + <!-- Control Sidebar --> + <aside class="control-sidebar control-sidebar-dark"> + <!-- Create the tabs --> + <ul class="nav nav-tabs nav-justified control-sidebar-tabs"> + <li class="active"><a href="#control-sidebar-home-tab" data-toggle="tab"><i class="fa fa-home"></i></a></li> + <li><a href="#control-sidebar-settings-tab" data-toggle="tab"><i class="fa fa-gears"></i></a></li> + </ul> + <!-- Tab panes --> + <div class="tab-content"> + <!-- Home tab content --> + <div class="tab-pane active" id="control-sidebar-home-tab"> + <h3 class="control-sidebar-heading">近期活动</h3> + <ul class="control-sidebar-menu"> + <li> + <a href="javascript::;"> + <i class="menu-icon fa fa-birthday-cake bg-red"></i> + <div class="menu-info"> + <h4 class="control-sidebar-subheading">张三今天过生日</h4> + <p>2015-09-10</p> + </div> + </a> + </li> + <li> + <a href="javascript::;"> + <i class="menu-icon fa fa-user bg-yellow"></i> + <div class="menu-info"> + <h4 class="control-sidebar-subheading">Frodo 更新了资料</h4> + <p>更新手机号码 +1(800)555-1234</p> + </div> + </a> + </li> + <li> + <a href="javascript::;"> + <i class="menu-icon fa fa-envelope-o bg-light-blue"></i> + <div class="menu-info"> + <h4 class="control-sidebar-subheading">Nora 加入邮件列表</h4> + <p>nora@example.com</p> + </div> + </a> + </li> + <li> + <a href="javascript::;"> + <i class="menu-icon fa fa-file-code-o bg-green"></i> + <div class="menu-info"> + <h4 class="control-sidebar-subheading">001号定时作业调度</h4> + <p>5秒前执行</p> + </div> + </a> + </li> + </ul> + <!-- /.control-sidebar-menu --> + </div> + <!-- /.tab-pane --> + + <!-- Settings tab content --> + <div class="tab-pane" id="control-sidebar-settings-tab"> + <form method="post"> + <h3 class="control-sidebar-heading">个人设置</h3> + <div class="form-group"> + <label class="control-sidebar-subheading"> 左侧菜单自适应 + <input type="checkbox" class="pull-right" checked> + </label> + <p>左侧菜单栏样式自适应</p> + </div> + <!-- /.form-group --> + + </form> + </div> + <!-- /.tab-pane --> + </div> + </aside> + <!-- /.control-sidebar --> + <!-- Add the sidebar's background. This div must be placed immediately after the control sidebar --> + <div class="control-sidebar-bg"></div> +</#macro> + +<#macro commonFooter > + <footer class="main-footer"> + Powered by <b>XXL-JOB</b> ${I18n.admin_version} + <div class="pull-right hidden-xs"> + <strong>Copyright © 2015-${.now?string('yyyy')} + <a href="https://www.xuxueli.com/" target="_blank" >xuxueli</a> + + <a href="https://github.com/xuxueli/xxl-job" target="_blank" >github</a> + </strong><!-- All rights reserved. --> + </div> + </footer> +</#macro> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl new file mode 100644 index 0000000..1409fc5 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> + <#import "./common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if> "> +<div class="wrapper"> + <!-- header --> + <@netCommon.commonHeader /> + <!-- left --> + <@netCommon.commonLeft "help" /> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + <section class="content-header"> + <h1>${I18n.job_help}</h1> + </section> + + <!-- Main content --> + <section class="content"> + <div class="callout callout-info"> + <h4>${I18n.admin_name_full}</h4> + <br> + <p> + <a target="_blank" href="https://github.com/xuxueli/xxl-job">Github</a> + <iframe src="https://ghbtns.com/github-btn.html?user=xuxueli&repo=xxl-job&type=star&count=true" frameborder="0" scrolling="0" width="170px" height="20px" style="margin-bottom:-5px;"></iframe> + <br><br> + <a target="_blank" href="https://www.xuxueli.com/xxl-job/">${I18n.job_help_document}</a> + <br><br> + + </p> + <p></p> + </div> + </section> + <!-- /.content --> + </div> + <!-- /.content-wrapper --> + + <!-- footer --> + <@netCommon.commonFooter /> +</div> +<@netCommon.commonScript /> +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl new file mode 100644 index 0000000..d642f4e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<html> +<head> + <#import "./common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <!-- daterangepicker --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css"> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if> "> +<div class="wrapper"> + <!-- header --> + <@netCommon.commonHeader /> + <!-- left --> + <@netCommon.commonLeft "index" /> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + <section class="content-header"> + <h1>${I18n.job_dashboard_name}</h1> + <!-- + <h1>运行报表<small>任务调度中心</small></h1> + <ol class="breadcrumb"> + <li><a><i class="fa fa-dashboard"></i>调度中心</a></li> + <li class="active">使用教程</li> + </ol> + --> + </section> + + <!-- Main content --> + <section class="content"> + + <!-- 任务信息 --> + <div class="row"> + + <#-- 任务信息 --> + <div class="col-md-4 col-sm-6 col-xs-12"> + <div class="info-box bg-aqua"> + <span class="info-box-icon"><i class="fa fa-flag-o"></i></span> + + <div class="info-box-content"> + <span class="info-box-text">${I18n.job_dashboard_job_num}</span> + <span class="info-box-number">${jobInfoCount}</span> + + <div class="progress"> + <div class="progress-bar" style="width: 100%"></div> + </div> + <span class="progress-description">${I18n.job_dashboard_job_num_tip}</span> + </div> + </div> + </div> + + <#-- 调度信息 --> + <div class="col-md-4 col-sm-6 col-xs-12" > + <div class="info-box bg-yellow"> + <span class="info-box-icon"><i class="fa fa-calendar"></i></span> + + <div class="info-box-content"> + <span class="info-box-text">${I18n.job_dashboard_trigger_num}</span> + <span class="info-box-number">${jobLogCount}</span> + + <div class="progress"> + <div class="progress-bar" style="width: 100%" ></div> + </div> + <span class="progress-description"> + ${I18n.job_dashboard_trigger_num_tip} + <#--<#if jobLogCount gt 0> + 调度成功率:${(jobLogSuccessCount*100/jobLogCount)?string("0.00")}<small>%</small> + </#if>--> + </span> + </div> + </div> + </div> + + <#-- 执行器 --> + <div class="col-md-4 col-sm-6 col-xs-12"> + <div class="info-box bg-green"> + <span class="info-box-icon"><i class="fa ion-ios-settings-strong"></i></span> + + <div class="info-box-content"> + <span class="info-box-text">${I18n.job_dashboard_jobgroup_num}</span> + <span class="info-box-number">${executorCount}</span> + + <div class="progress"> + <div class="progress-bar" style="width: 100%"></div> + </div> + <span class="progress-description">${I18n.job_dashboard_jobgroup_num_tip}</span> + </div> + </div> + </div> + + </div> + + <#-- 调度报表:时间区间筛选,左侧折线图 + 右侧饼图 --> + <div class="row"> + <div class="col-md-12"> + <div class="box"> + <div class="box-header with-border"> + <h3 class="box-title">${I18n.job_dashboard_report}</h3> + <#--<input type="text" class="form-control" id="filterTime" readonly >--> + + <!-- tools box --> + <div class="pull-right box-tools"> + <button type="button" class="btn btn-primary btn-sm daterange pull-right" data-toggle="tooltip" id="filterTime" > + <i class="fa fa-calendar"></i> + </button> + <#--<button type="button" class="btn btn-primary btn-sm pull-right" data-widget="collapse" data-toggle="tooltip" title="" style="margin-right: 5px;" data-original-title="Collapse"> + <i class="fa fa-minus"></i> + </button>--> + </div> + <!-- /. tools --> + + </div> + <div class="box-body"> + <div class="row"> + <#-- 左侧折线图 --> + <div class="col-md-8"> + <div id="lineChart" style="height: 350px;"></div> + </div> + <#-- 右侧饼图 --> + <div class="col-md-4"> + <div id="pieChart" style="height: 350px;"></div> + </div> + </div> + </div> + </div> + </div> + </div> + + </section> + <!-- /.content --> + </div> + <!-- /.content-wrapper --> + + <!-- footer --> + <@netCommon.commonFooter /> +</div> +<@netCommon.commonScript /> +<!-- daterangepicker --> +<script src="${request.contextPath}/static/adminlte/bower_components/moment/moment.min.js"></script> +<script src="${request.contextPath}/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js"></script> +<#-- echarts --> +<script src="${request.contextPath}/static/plugins/echarts/echarts.common.min.js"></script> +<script src="${request.contextPath}/static/js/index.js"></script> +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl new file mode 100644 index 0000000..a386b28 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<html> +<head> + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <link rel="stylesheet" href="${request.contextPath}/static/plugins/codemirror/lib/codemirror.css"> + <link rel="stylesheet" href="${request.contextPath}/static/plugins/codemirror/addon/hint/show-hint.css"> + <title>${I18n.admin_name}</title> + <style type="text/css"> + .CodeMirror { + font-size:16px; + width: 100%; + height: 100%; + /*bottom: 0; + top: 0px;*/ + position: absolute; + } + </style> +</head> +<body class="skin-blue fixed layout-top-nav"> + + <div class="wrapper"> + + <header class="main-header"> + <nav class="navbar navbar-static-top"> + <div class="container"> + <#-- icon --> + <div class="navbar-header"> + <a class="navbar-brand"><b>Web</b>IDE</a> + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse"> + <i class="fa fa-bars"></i> + </button> + </div> + + <#-- left nav --> + <div class="collapse navbar-collapse pull-left" id="navbar-collapse"> + <ul class="nav navbar-nav"> + <li class="active" ><a href="javascript:;"> + <span class="sr-only">(current)</span> + 【<#list GlueTypeEnum as item><#if item == jobInfo.glueType>${item.desc}</#if></#list>】 + ${jobInfo.jobDesc} + </a></li> + </ul> + </div> + + <#-- right nav --> + <div class="navbar-custom-menu"> + <ul class="nav navbar-nav"> + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">${I18n.jobinfo_glue_rollback} <span class="caret"></span></a> + <ul class="dropdown-menu" role="menu"> + <li <#if jobLogGlues?exists && jobLogGlues?size gt 0 >style="display: none;"</#if> > + <a href="javascript:;" class="source_version" version="version_now" glueType="${jobInfo.glueType}" > + <#list GlueTypeEnum as item><#if item == jobInfo.glueType>${item.desc}</#if></#list>: ${jobInfo.glueRemark} + </a> + </li> + <textarea id="version_now" style="display:none;" >${jobInfo.glueSource}</textarea> + <#if jobLogGlues?exists && jobLogGlues?size gt 0 > + <#list jobLogGlues as glue> + <li> + <a href="javascript:;" class="source_version" version="version_${glue.id}" glueType="${glue.glueType}" > + <#list GlueTypeEnum as item><#if item == glue.glueType>${item.desc}</#if></#list>: ${glue.glueRemark} + </a> + </li> + <textarea id="version_${glue.id}" style="display:none;" >${glue.glueSource}</textarea> + </#list> + </#if> + </ul> + </li> + <li id="save" > + <a href="javascript:;" > + <i class="fa fa-fw fa-save" ></i> + ${I18n.system_save} + </a> + </li> + <li> + <a href="javascript:window.close();" > + <i class="fa fa-fw fa-close" ></i> + ${I18n.system_close} + </a> + </li> + </ul> + </div> + + </div> + </nav> + </header> + + <div class="content-wrapper" id="ideWindow" ></div> + + <!-- footer --> + <#--<@netCommon.commonFooter />--> + </div> + + <!-- 保存.模态框 --> + <div class="modal fade" id="saveModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog "> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" ><i class="fa fa-fw fa-save"></i>${I18n.system_save}</h4> + </div> + <div class="modal-body"> + <div class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_glue_remark}<font color="red">*</font></label> + <div class="col-sm-10"><input type="text" class="form-control" id="glueRemark" placeholder="${I18n.system_please_input}${I18n.jobinfo_glue_remark}" maxlength="64" ></div> + </div> + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="button" class="btn btn-primary ok" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + +<@netCommon.commonScript /> + + + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/clike/clike.js" /> + <#assign glueTypeIdeMode = "text/x-java" /> + + <#if jobInfo.glueType == "GLUE_GROOVY" > + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/clike/clike.js" /> + <#assign glueTypeIdeMode = "text/x-java" /> + <#elseif jobInfo.glueType == "GLUE_SHELL" > + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/shell/shell.js" /> + <#assign glueTypeIdeMode = "text/x-sh" /> + <#elseif jobInfo.glueType == "GLUE_PYTHON" > + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/python/python.js" /> + <#assign glueTypeIdeMode = "text/x-python" /> + <#elseif jobInfo.glueType == "GLUE_PHP" > + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/php/php.js" /> + <#assign glueTypeIdeMode = "text/x-php" /> + <#assign glueTypeModeSrc02 = "${request.contextPath}/static/plugins/codemirror/mode/clike/clike.js" /> + <#elseif jobInfo.glueType == "GLUE_NODEJS" > + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/javascript/javascript.js" /> + <#assign glueTypeIdeMode = "text/javascript" /> + <#elseif jobInfo.glueType == "GLUE_POWERSHELL" > + <#assign glueTypeModeSrc = "${request.contextPath}/static/plugins/codemirror/mode/powershell/powershell.js" /> + <#assign glueTypeIdeMode = "powershell" /> + </#if> + + +<script src="${request.contextPath}/static/plugins/codemirror/lib/codemirror.js"></script> +<script src="${glueTypeModeSrc}"></script> +<#if glueTypeModeSrc02?exists> + <script src="${glueTypeModeSrc02}"></script> +</#if> +<script src="${request.contextPath}/static/plugins/codemirror/addon/hint/show-hint.js"></script> +<script src="${request.contextPath}/static/plugins/codemirror/addon/hint/anyword-hint.js"></script> + +<script> +var id = '${jobInfo.id}'; +var ideMode = '${glueTypeIdeMode}'; +</script> +<script src="${request.contextPath}/static/js/jobcode.index.1.js"></script> + +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl new file mode 100644 index 0000000..778df9e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl @@ -0,0 +1,172 @@ +<!DOCTYPE html> +<html> +<head> + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <!-- DataTables --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css"> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if> "> +<div class="wrapper"> + <!-- header --> + <@netCommon.commonHeader /> + <!-- left --> + <@netCommon.commonLeft "jobgroup" /> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + <section class="content-header"> + <h1>${I18n.jobgroup_name}</h1> + </section> + + <!-- Main content --> + <section class="content"> + + <div class="row"> + <div class="col-xs-3"> + <div class="input-group"> + <span class="input-group-addon">AppName</span> + <input type="text" class="form-control" id="appname" placeholder="${I18n.system_please_input}AppName" > + </div> + </div> + <div class="col-xs-3"> + <div class="input-group"> + <span class="input-group-addon">${I18n.jobgroup_field_title}</span> + <input type="text" class="form-control" id="title" placeholder="${I18n.jobgroup_field_title}" > + </div> + </div> + <div class="col-xs-2"> + <button class="btn btn-block btn-info" id="searchBtn">${I18n.system_search}</button> + </div> + <div class="col-xs-2"> + <button class="btn btn-block btn-success add" type="button">${I18n.jobinfo_field_add}</button> + </div> + </div> + + <div class="row"> + <div class="col-xs-12"> + <div class="box"> + <div class="box-body"> + <table id="jobgroup_list" class="table table-bordered table-striped display" width="100%" > + <thead> + <tr> + <th name="id" >ID</th> + <th name="appname" >AppName</th> + <th name="title" >${I18n.jobgroup_field_title}</th> + <th name="addressType" >${I18n.jobgroup_field_addressType}</th> + <th name="registryList" >OnLine ${I18n.jobgroup_field_registryList}</th> + <th>${I18n.system_opt}</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + </div> + </div> + </div> + </div> + </section> + </div> + + <!-- 新增.模态框 --> + <div class="modal fade" id="addModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog "> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.jobgroup_add}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">AppName<font color="red">*</font></label> + <div class="col-sm-10"><input type="text" class="form-control" name="appname" placeholder="${I18n.system_please_input}AppName" maxlength="64" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobgroup_field_title}<font color="red">*</font></label> + <div class="col-sm-10"><input type="text" class="form-control" name="title" placeholder="${I18n.system_please_input}${I18n.jobgroup_field_title}" maxlength="12" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobgroup_field_addressType}<font color="red">*</font></label> + <div class="col-sm-10"> + <input type="radio" name="addressType" value="0" checked />${I18n.jobgroup_field_addressType_0} + + <input type="radio" name="addressType" value="1" />${I18n.jobgroup_field_addressType_1} + </div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobgroup_field_registryList}<font color="red">*</font></label> + <div class="col-sm-10"> + <textarea class="textarea" name="addressList" maxlength="20000" placeholder="${I18n.jobgroup_field_registryList_placeholder}" readonly="readonly" style="background-color:#eee; width: 100%; height: 100px; font-size: 14px; line-height: 15px; border: 1px solid #dddddd; padding: 5px;"></textarea> + </div> + </div> + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + </div> + </div> + </form> + </div> + </div> + </div> + </div> + + <!-- 更新.模态框 --> + <div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog "> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.jobgroup_edit}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">AppName<font color="red">*</font></label> + <div class="col-sm-10"><input type="text" class="form-control" name="appname" placeholder="${I18n.system_please_input}AppName" maxlength="64" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobgroup_field_title}<font color="red">*</font></label> + <div class="col-sm-10"><input type="text" class="form-control" name="title" placeholder="${I18n.system_please_input}${I18n.jobgroup_field_title}" maxlength="12" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobgroup_field_addressType}<font color="red">*</font></label> + <div class="col-sm-10"> + <input type="radio" name="addressType" value="0" />${I18n.jobgroup_field_addressType_0} + + <input type="radio" name="addressType" value="1" />${I18n.jobgroup_field_addressType_1} + </div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobgroup_field_registryList}<font color="red">*</font></label> + <div class="col-sm-10"> + <textarea class="textarea" name="addressList" maxlength="20000" placeholder="${I18n.jobgroup_field_registryList_placeholder}" readonly="readonly" style="background-color:#eee; width: 100%; height: 100px; font-size: 14px; line-height: 15px; border: 1px solid #dddddd; padding: 5px;"></textarea> + </div> + </div> + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + <input type="hidden" name="id" > + </div> + </div> + </form> + </div> + </div> + </div> + </div> + + <!-- footer --> + <@netCommon.commonFooter /> +</div> + +<@netCommon.commonScript /> +<!-- DataTables --> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net/js/jquery.dataTables.min.js"></script> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script> +<script src="${request.contextPath}/static/js/jobgroup.index.1.js"></script> +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl new file mode 100644 index 0000000..3a5d7d8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl @@ -0,0 +1,540 @@ +<!DOCTYPE html> +<html> +<head> + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <!-- DataTables --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css"> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if>"> +<div class="wrapper"> + <!-- header --> + <@netCommon.commonHeader /> + <!-- left --> + <@netCommon.commonLeft "jobinfo" /> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + <section class="content-header"> + <h1>${I18n.jobinfo_name}</h1> + </section> + + <!-- Main content --> + <section class="content"> + + <div class="row"> + <div class="col-xs-3"> + <div class="input-group"> + <span class="input-group-addon">${I18n.jobinfo_field_jobgroup}</span> + <select class="form-control" id="jobGroup" > + <#list JobGroupList as group> + <option value="${group.id}" <#if jobGroup==group.id>selected</#if> >${group.title}</option> + </#list> + </select> + </div> + </div> + <div class="col-xs-1"> + <div class="input-group"> + <select class="form-control" id="triggerStatus" > + <option value="-1" >${I18n.system_all}</option> + <option value="0" >${I18n.jobinfo_opt_stop}</option> + <option value="1" >${I18n.jobinfo_opt_start}</option> + </select> + </div> + </div> + <div class="col-xs-2"> + <div class="input-group"> + <input type="text" class="form-control" id="jobDesc" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_jobdesc}" > + </div> + </div> + <div class="col-xs-2"> + <div class="input-group"> + <input type="text" class="form-control" id="executorHandler" placeholder="${I18n.system_please_input}JobHandler" > + </div> + </div> + <div class="col-xs-2"> + <div class="input-group"> + <input type="text" class="form-control" id="author" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_author}" > + </div> + </div> + <div class="col-xs-1"> + <button class="btn btn-block btn-info" id="searchBtn">${I18n.system_search}</button> + </div> + <div class="col-xs-1"> + <button class="btn btn-block btn-success add" type="button">${I18n.jobinfo_field_add}</button> + </div> + </div> + + <div class="row"> + <div class="col-xs-12"> + <div class="box"> + <#--<div class="box-header hide"> + <h3 class="box-title">调度列表</h3> + </div>--> + <div class="box-body" > + <table id="job_list" class="table table-bordered table-striped" width="100%" > + <thead> + <tr> + <th name="id" >${I18n.jobinfo_field_id}</th> + <th name="jobGroup" >${I18n.jobinfo_field_jobgroup}</th> + <th name="jobDesc" >${I18n.jobinfo_field_jobdesc}</th> + <th name="scheduleType" >${I18n.schedule_type}</th> + <th name="glueType" >${I18n.jobinfo_field_gluetype}</th> + <th name="executorParam" >${I18n.jobinfo_field_executorparam}</th> + <th name="addTime" >addTime</th> + <th name="updateTime" >updateTime</th> + <th name="author" >${I18n.jobinfo_field_author}</th> + <th name="alarmEmail" >${I18n.jobinfo_field_alarmemail}</th> + <th name="triggerStatus" >${I18n.system_status}</th> + <th>${I18n.system_opt}</th> + </tr> + </thead> + <tbody></tbody> + <tfoot></tfoot> + </table> + </div> + </div> + </div> + </div> + </section> + </div> + + <!-- footer --> + <@netCommon.commonFooter /> +</div> + +<!-- job新增.模态框 --> +<div class="modal fade" id="addModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.jobinfo_field_add}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_base}</p> <#-- 基础信息 --> + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_jobgroup}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="jobGroup" > + <#list JobGroupList as group> + <option value="${group.id}" <#if jobGroup==group.id>selected</#if> >${group.title}</option> + </#list> + </select> + </div> + + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_jobdesc}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="jobDesc" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_jobdesc}" maxlength="50" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_author}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="author" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_author}" maxlength="50" ></div> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_alarmemail}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="alarmEmail" placeholder="${I18n.jobinfo_field_alarmemail_placeholder}" maxlength="100" ></div> + </div> + + <br> + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_schedule}</p> <#-- 调度 --> + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.schedule_type}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control scheduleType" name="scheduleType" > + <#list ScheduleTypeEnum as item> + <option value="${item}" <#if 'CRON' == item >selected</#if> >${item.title}</option> + </#list> + </select> + </div> + + <input type="hidden" name="scheduleConf" /> + <div class="schedule_conf schedule_conf_NONE" style="display: none" > + </div> + <div class="schedule_conf schedule_conf_CRON" > + <label for="lastname" class="col-sm-2 control-label">Cron<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="schedule_conf_CRON" placeholder="${I18n.system_please_input}Cron" maxlength="128" ></div> + </div> + <div class="schedule_conf schedule_conf_FIX_RATE" style="display: none" > + <label for="lastname" class="col-sm-2 control-label">${I18n.schedule_type_fix_rate}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="schedule_conf_FIX_RATE" placeholder="${I18n.system_please_input} ( Second )" maxlength="10" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + </div> + <div class="schedule_conf schedule_conf_FIX_DELAY" style="display: none" > + <label for="lastname" class="col-sm-2 control-label">${I18n.schedule_type_fix_delay}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="schedule_conf_FIX_DELAY" placeholder="${I18n.system_please_input} ( Second )" maxlength="10" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + </div> + </div> + + <br> + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_job}</p> <#-- 任务配置 --> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_gluetype}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control glueType" name="glueType" > + <#list GlueTypeEnum as item> + <option value="${item}" >${item.desc}</option> + </#list> + </select> + </div> + <label for="firstname" class="col-sm-2 control-label">JobHandler<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="executorHandler" placeholder="${I18n.system_please_input}JobHandler" maxlength="100" ></div> + </div> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorparam}<font color="black">*</font></label> + <div class="col-sm-10"> + <textarea class="textarea form-control" name="executorParam" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_executorparam}" maxlength="512" style="height: 63px; line-height: 1.2;"></textarea> + </div> + </div> + + <br> + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_advanced}</p> <#-- 高级配置 --> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorRouteStrategy}<font color="black">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="executorRouteStrategy" > + <#list ExecutorRouteStrategyEnum as item> + <option value="${item}" >${item.title}</option> + </#list> + </select> + </div> + + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_childJobId}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="childJobId" placeholder="${I18n.jobinfo_field_childJobId_placeholder}" maxlength="100" ></div> + </div> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.misfire_strategy}<font color="black">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="misfireStrategy" > + <#list MisfireStrategyEnum as item> + <option value="${item}" <#if 'DO_NOTHING' == item >selected</#if> >${item.title}</option> + </#list> + </select> + </div> + + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorBlockStrategy}<font color="black">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="executorBlockStrategy" > + <#list ExecutorBlockStrategyEnum as item> + <option value="${item}" >${item.title}</option> + </#list> + </select> + </div> + </div> + + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_timeout}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="executorTimeout" placeholder="${I18n.jobinfo_field_executorTimeout_placeholder}" maxlength="6" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorFailRetryCount}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="executorFailRetryCount" placeholder="${I18n.jobinfo_field_executorFailRetryCount_placeholder}" maxlength="4" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + </div> + + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + </div> + </div> + +<input type="hidden" name="glueRemark" value="GLUE代码初始化" > +<textarea name="glueSource" style="display:none;" ></textarea> +<textarea class="glueSource_java" style="display:none;" > +package com.xxl.job.service.handler; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.IJobHandler; + +public class DemoGlueJobHandler extends IJobHandler { + + @Override + public void execute() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + } + +} +</textarea> +<textarea class="glueSource_shell" style="display:none;" > +#!/bin/bash +echo "xxl-job: hello shell" + +echo "${I18n.jobinfo_script_location}:$0" +echo "${I18n.jobinfo_field_executorparam}:$1" +echo "${I18n.jobinfo_shard_index} = $2" +echo "${I18n.jobinfo_shard_total} = $3" +<#--echo "参数数量:$#" +for param in $* +do + echo "参数 : $param" + sleep 1s +done--> + +echo "Good bye!" +exit 0 +</textarea> +<textarea class="glueSource_python" style="display:none;" > +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import time +import sys + +print "xxl-job: hello python" + +print "${I18n.jobinfo_script_location}:", sys.argv[0] +print "${I18n.jobinfo_field_executorparam}:", sys.argv[1] +print "${I18n.jobinfo_shard_index}:", sys.argv[2] +print "${I18n.jobinfo_shard_total}:", sys.argv[3] +<#--for i in range(1, len(sys.argv)): + time.sleep(1) + print "参数", i, sys.argv[i]--> + +print "Good bye!" +exit(0) +<#-- +import logging +logging.basicConfig(level=logging.DEBUG) +logging.info("脚本文件:" + sys.argv[0]) +--> +</textarea> +<#--这里有问题,新建一个运行模式为 php 的任务后,GLUE 中没有下边的 php 代码--> +<textarea class="glueSource_php" style="display:none;" > +<?php + + echo "xxl-job: hello php \n"; + + echo "${I18n.jobinfo_script_location}:$argv[0] \n"; + echo "${I18n.jobinfo_field_executorparam}:$argv[1] \n"; + echo "${I18n.jobinfo_shard_index} = $argv[2] \n"; + echo "${I18n.jobinfo_shard_total} = $argv[3] \n"; + + echo "Good bye! \n"; + exit(0); + +?> +</textarea> +<textarea class="glueSource_nodejs" style="display:none;" > +#!/usr/bin/env node +console.log("xxl-job: hello nodejs") + +var arguments = process.argv + +console.log("${I18n.jobinfo_script_location}: " + arguments[1]) +console.log("${I18n.jobinfo_field_executorparam}: " + arguments[2]) +console.log("${I18n.jobinfo_shard_index}: " + arguments[3]) +console.log("${I18n.jobinfo_shard_total}: " + arguments[4]) +<#--for (var i = 2; i < arguments.length; i++){ + console.log("参数 %s = %s", (i-1), arguments[i]); +}--> + +console.log("Good bye!") +process.exit(0) +</textarea> +<textarea class="glueSource_powershell" style="display:none;" > +Write-Host "xxl-job: hello powershell" + +Write-Host "${I18n.jobinfo_script_location}: " $MyInvocation.MyCommand.Definition +Write-Host "${I18n.jobinfo_field_executorparam}: " + if ($args.Count -gt 2) { $args[0..($args.Count-3)] } +Write-Host "${I18n.jobinfo_shard_index}: " $args[$args.Count-2] +Write-Host "${I18n.jobinfo_shard_total}: " $args[$args.Count-1] + +Write-Host "Good bye!" +exit 0 +</textarea> + </form> + </div> + </div> + </div> +</div> + +<!-- 更新.模态框 --> +<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.jobinfo_field_update}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_base}</p> <#-- 基础信息 --> + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_jobgroup}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="jobGroup" > + <#list JobGroupList as group> + <option value="${group.id}" >${group.title}</option> + </#list> + </select> + </div> + + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_jobdesc}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="jobDesc" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_jobdesc}" maxlength="50" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_author}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="author" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_author}" maxlength="50" ></div> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_alarmemail}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="alarmEmail" placeholder="${I18n.jobinfo_field_alarmemail_placeholder}" maxlength="100" ></div> + </div> + + <br> + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_schedule}</p> <#-- 调度配置 --> + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.schedule_type}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control scheduleType" name="scheduleType" > + <#list ScheduleTypeEnum as item> + <option value="${item}" >${item.title}</option> + </#list> + </select> + </div> + + <input type="hidden" name="scheduleConf" /> + <div class="schedule_conf schedule_conf_NONE" style="display: none" > + </div> + <div class="schedule_conf schedule_conf_CRON" > + <label for="lastname" class="col-sm-2 control-label">Cron<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="schedule_conf_CRON" placeholder="${I18n.system_please_input}Cron" maxlength="128" ></div> + </div> + <div class="schedule_conf schedule_conf_FIX_RATE" style="display: none" > + <label for="lastname" class="col-sm-2 control-label">${I18n.schedule_type_fix_rate}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="schedule_conf_FIX_RATE" placeholder="${I18n.system_please_input} ( Second )" maxlength="10" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + </div> + <div class="schedule_conf schedule_conf_FIX_DELAY" style="display: none" > + <label for="lastname" class="col-sm-2 control-label">${I18n.schedule_type_fix_delay}<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="schedule_conf_FIX_DELAY" placeholder="${I18n.system_please_input} ( Second )" maxlength="10" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + </div> + </div> + + <br> + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_job}</p> <#-- 任务配置 --> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_gluetype}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control glueType" name="glueType" disabled > + <#list GlueTypeEnum as item> + <option value="${item}" >${item.desc}</option> + </#list> + </select> + </div> + <label for="firstname" class="col-sm-2 control-label">JobHandler<font color="red">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="executorHandler" placeholder="${I18n.system_please_input}JobHandler" maxlength="100" ></div> + </div> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorparam}<font color="black">*</font></label> + <div class="col-sm-10"> + <textarea class="textarea form-control" name="executorParam" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_executorparam}" maxlength="512" style="height: 63px; line-height: 1.2;"></textarea> + </div> + </div> + + <br> + <p style="margin: 0 0 10px;text-align: left;border-bottom: 1px solid #e5e5e5;color: gray;">${I18n.jobinfo_conf_advanced}</p> <#-- 高级配置 --> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorRouteStrategy}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="executorRouteStrategy" > + <#list ExecutorRouteStrategyEnum as item> + <option value="${item}" >${item.title}</option> + </#list> + </select> + </div> + + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_childJobId}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="childJobId" placeholder="${I18n.jobinfo_field_childJobId_placeholder}" maxlength="100" ></div> + </div> + + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.misfire_strategy}<font color="black">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="misfireStrategy" > + <#list MisfireStrategyEnum as item> + <option value="${item}" <#if 'DO_NOTHING' == item >selected</#if> >${item.title}</option> + </#list> + </select> + </div> + + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorBlockStrategy}<font color="red">*</font></label> + <div class="col-sm-4"> + <select class="form-control" name="executorBlockStrategy" > + <#list ExecutorBlockStrategyEnum as item> + <option value="${item}" >${item.title}</option> + </#list> + </select> + </div> + </div> + + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_timeout}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="executorTimeout" placeholder="${I18n.jobinfo_field_executorTimeout_placeholder}" maxlength="6" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + <label for="lastname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorFailRetryCount}<font color="black">*</font></label> + <div class="col-sm-4"><input type="text" class="form-control" name="executorFailRetryCount" placeholder="${I18n.jobinfo_field_executorFailRetryCount_placeholder}" maxlength="4" onkeyup="this.value=this.value.replace(/\D/g,'')" onafterpaste="this.value=this.value.replace(/\D/g,'')" ></div> + </div> + + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + <input type="hidden" name="id" > + </div> + </div> + + </form> + </div> + </div> + </div> +</div> + +<#-- trigger --> +<div class="modal fade" id="jobTriggerModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog "> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.jobinfo_opt_run}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobinfo_field_executorparam}<font color="black">*</font></label> + <div class="col-sm-10"> + <textarea class="textarea form-control" name="executorParam" placeholder="${I18n.system_please_input}${I18n.jobinfo_field_executorparam}" maxlength="512" style="height: 63px; line-height: 1.2;"></textarea> + </div> + </div> + <div class="form-group"> + <label for="firstname" class="col-sm-2 control-label">${I18n.jobgroup_field_registryList}<font color="black">*</font></label> + <div class="col-sm-10"> + <textarea class="textarea form-control" name="addressList" placeholder="${I18n.jobinfo_opt_run_tips}" maxlength="512" style="height: 63px; line-height: 1.2;"></textarea> + </div> + </div> + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="button" class="btn btn-primary ok" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + <input type="hidden" name="id" > + </div> + </div> + </form> + </div> + </div> + </div> +</div> + +<@netCommon.commonScript /> +<!-- DataTables --> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net/js/jquery.dataTables.min.js"></script> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script> +<!-- moment --> +<script src="${request.contextPath}/static/adminlte/bower_components/moment/moment.min.js"></script> +<#-- cronGen --> +<script src="${request.contextPath}/static/plugins/cronGen/cronGen<#if I18n.admin_i18n?default('')?length gt 0 >_${I18n.admin_i18n}</#if>.js"></script> +<script src="${request.contextPath}/static/js/jobinfo.index.1.js"></script> +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl new file mode 100644 index 0000000..3881cfa --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> +<head> + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue layout-top-nav"> + +<div class="wrapper"> + + <header class="main-header"> + <nav class="navbar navbar-static-top"> + <div class="container"> + <#-- icon --> + <div class="navbar-header"> + <a class="navbar-brand"><b>${I18n.joblog_rolling_log}</b> Console</a> + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse"> + <i class="fa fa-bars"></i> + </button> + </div> + + <#-- left nav --> + <div class="collapse navbar-collapse pull-left" id="navbar-collapse"> + <ul class="nav navbar-nav"> + <#--<li class="active" ><a href="javascript:;">任务:<span class="sr-only">(current)</span></a></li>--> + </ul> + </div> + + <#-- right nav --> + <div class="navbar-custom-menu"> + <ul class="nav navbar-nav"> + <li> + <a href="javascript:window.location.reload();" > + <i class="fa fa-fw fa-refresh" ></i> + ${I18n.joblog_rolling_log_refresh} + </a> + </li> + </ul> + </div> + + </div> + </nav> + </header> + + <div class="content-wrapper" > + <section class="content"> + <pre style="font-size:12px;position:relative;" > + <div id="logConsole"></div> + <li class="fa fa-refresh fa-spin" style="font-size: 20px;float: left;" id="logConsoleRunning" ></li> + </pre> + </section> + </div> + + <!-- footer --> + <@netCommon.commonFooter /> + +</div> + +<@netCommon.commonScript /> +<script> + // 参数 + var triggerCode = '${triggerCode}'; + var handleCode = '${handleCode}'; + var executorAddress = '${executorAddress!}'; + var triggerTime = '${triggerTime?c}'; + var logId = '${logId}'; +</script> +<script src="${request.contextPath}/static/js/joblog.detail.1.js"></script> + +</body> +</html> \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl new file mode 100644 index 0000000..a2e983d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl @@ -0,0 +1,180 @@ +<!DOCTYPE html> +<html> +<head> + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <!-- DataTables --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css"> + <!-- daterangepicker --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css"> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if> "> +<div class="wrapper"> + <!-- header --> + <@netCommon.commonHeader /> + <!-- left --> + <@netCommon.commonLeft "joblog" /> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + <section class="content-header"> + <h1>${I18n.joblog_name}</h1> + </section> + + <!-- Main content --> + <section class="content"> + <div class="row"> + <div class="col-xs-2"> + <div class="input-group"> + <span class="input-group-addon">${I18n.jobinfo_field_jobgroup}</span> + <select class="form-control" id="jobGroup" paramVal="<#if jobInfo?exists>${jobInfo.jobGroup}</#if>" > + <#if Request["XXL_JOB_LOGIN_IDENTITY"].role == 1> + <option value="0" >${I18n.system_all}</option> <#-- 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup --> + </#if> + <#list JobGroupList as group> + <option value="${group.id}" >${group.title}</option> + </#list> + </select> + </div> + </div> + <div class="col-xs-2"> + <div class="input-group"> + <span class="input-group-addon">${I18n.jobinfo_job}</span> + <select class="form-control" id="jobId" paramVal="<#if jobInfo?exists>${jobInfo.id}</#if>" > + <option value="0" >${I18n.system_all}</option> + </select> + </div> + </div> + + <div class="col-xs-2"> + <div class="input-group"> + <span class="input-group-addon">${I18n.joblog_status}</span> + <select class="form-control" id="logStatus" > + <option value="-1" >${I18n.joblog_status_all}</option> + <option value="1" >${I18n.joblog_status_suc}</option> + <option value="2" >${I18n.joblog_status_fail}</option> + <option value="3" >${I18n.joblog_status_running}</option> + </select> + </div> + </div> + + <div class="col-xs-4"> + <div class="input-group"> + <span class="input-group-addon"> + ${I18n.joblog_field_triggerTime} + </span> + <input type="text" class="form-control" id="filterTime" readonly > + </div> + </div> + + <div class="col-xs-1"> + <button class="btn btn-block btn-info" id="searchBtn">${I18n.system_search}</button> + </div> + + <div class="col-xs-1"> + <button class="btn btn-block btn-default" id="clearLog">${I18n.joblog_clean}</button> + </div> + </div> + + <div class="row"> + <div class="col-xs-12"> + <div class="box"> + <#--<div class="box-header hide"><h3 class="box-title">调度日志</h3></div>--> + <div class="box-body"> + <table id="joblog_list" class="table table-bordered table-striped display" width="100%" > + <thead> + <tr> + <th name="jobId" >${I18n.jobinfo_field_id}</th> + <th name="jobGroup" >jobGroup</th> + <#--<th name="executorAddress" >执行器地址</th> + <th name="glueType" >运行模式</th> + <th name="executorParam" >任务参数</th>--> + <th name="triggerTime" >${I18n.joblog_field_triggerTime}</th> + <th name="triggerCode" >${I18n.joblog_field_triggerCode}</th> + <th name="triggerMsg" >${I18n.joblog_field_triggerMsg}</th> + <th name="handleTime" >${I18n.joblog_field_handleTime}</th> + <th name="handleCode" >${I18n.joblog_field_handleCode}</th> + <th name="handleMsg" >${I18n.joblog_field_handleMsg}</th> + <th name="handleMsg" >${I18n.system_opt}</th> + </tr> + </thead> + <tbody></tbody> + </table> + </div> + </div> + </div> + </div> + </section> + </div> + + <!-- footer --> + <@netCommon.commonFooter /> +</div> + +<!-- 日志清理.模态框 --> +<div class="modal fade" id="clearLogModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.joblog_clean_log}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label class="col-sm-3 control-label">${I18n.jobinfo_field_jobgroup}:</label> + <div class="col-sm-9"> + <input type="text" class="form-control jobGroupText" readonly > + <input type="hidden" name="jobGroup" > + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">${I18n.jobinfo_job}:</label> + <div class="col-sm-9"> + <input type="text" class="form-control jobIdText" readonly > + <input type="hidden" name="jobId" > + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">${I18n.joblog_clean_type}:</label> + <div class="col-sm-9"> + <select class="form-control" name="type" > + <option value="1" >${I18n.joblog_clean_type_1}</option> + <option value="2" >${I18n.joblog_clean_type_2}</option> + <option value="3" >${I18n.joblog_clean_type_3}</option> + <option value="4" >${I18n.joblog_clean_type_4}</option> + <option value="5" >${I18n.joblog_clean_type_5}</option> + <option value="6" >${I18n.joblog_clean_type_6}</option> + <option value="7" >${I18n.joblog_clean_type_7}</option> + <option value="8" >${I18n.joblog_clean_type_8}</option> + <option value="9" >${I18n.joblog_clean_type_9}</option> + </select> + </div> + </div> + + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="button" class="btn btn-primary ok" >${I18n.system_ok}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + </div> + </div> + </form> + </div> + </div> + </div> +</div> + +<@netCommon.commonScript /> +<!-- DataTables --> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net/js/jquery.dataTables.min.js"></script> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script> +<!-- daterangepicker --> +<script src="${request.contextPath}/static/adminlte/bower_components/moment/moment.min.js"></script> +<script src="${request.contextPath}/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js"></script> +<script src="${request.contextPath}/static/js/joblog.index.1.js"></script> +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl new file mode 100644 index 0000000..c3f6963 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <#import "./common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/plugins/iCheck/square/blue.css"> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition login-page"> + <div class="login-box"> + <div class="login-logo"> + <a><b>XXL</b>JOB</a> + </div> + <form id="loginForm" method="post" > + <div class="login-box-body"> + <p class="login-box-msg">${I18n.admin_name}</p> + <div class="form-group has-feedback"> + <input type="text" name="userName" class="form-control" placeholder="${I18n.login_username_placeholder}" maxlength="18" > + <span class="glyphicon glyphicon-envelope form-control-feedback"></span> + </div> + <div class="form-group has-feedback"> + <input type="password" name="password" class="form-control" placeholder="${I18n.login_password_placeholder}" maxlength="18" > + <span class="glyphicon glyphicon-lock form-control-feedback"></span> + </div> + <div class="row"> + <div class="col-xs-8"> + <div class="checkbox icheck"> + <label> + <input type="checkbox" name="ifRemember" > ${I18n.login_remember_me} + </label> + </div> + </div><!-- /.col --> + <div class="col-xs-4"> + <button type="submit" class="btn btn-primary btn-block btn-flat">${I18n.login_btn}</button> + </div> + </div> + </div> + </form> + </div> +<@netCommon.commonScript /> +<script src="${request.contextPath}/static/adminlte/plugins/iCheck/icheck.min.js"></script> +<script src="${request.contextPath}/static/js/login.1.js"></script> + +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl b/xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl new file mode 100644 index 0000000..0120398 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl @@ -0,0 +1,188 @@ +<!DOCTYPE html> +<html> +<head> + <#import "../common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> + <!-- DataTables --> + <link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css"> + <title>${I18n.admin_name}</title> +</head> +<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if>"> +<div class="wrapper"> + <!-- header --> + <@netCommon.commonHeader /> + <!-- left --> + <@netCommon.commonLeft "user" /> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + <section class="content-header"> + <h1>${I18n.user_manage}</h1> + </section> + + <!-- Main content --> + <section class="content"> + + <div class="row"> + <div class="col-xs-3"> + <div class="input-group"> + <span class="input-group-addon">${I18n.user_role}</span> + <select class="form-control" id="role" > + <option value="-1" >${I18n.system_all}</option> + <option value="1" >${I18n.user_role_admin}</option> + <option value="0" >${I18n.user_role_normal}</option> + </select> + </div> + </div> + <div class="col-xs-3"> + <div class="input-group"> + <span class="input-group-addon">${I18n.user_username}</span> + <input type="text" class="form-control" id="username" autocomplete="on" > + </div> + </div> + <div class="col-xs-1"> + <button class="btn btn-block btn-info" id="searchBtn">${I18n.system_search}</button> + </div> + <div class="col-xs-2"> + <button class="btn btn-block btn-success add" type="button">${I18n.user_add}</button> + </div> + </div> + + <div class="row"> + <div class="col-xs-12"> + <div class="box"> + <div class="box-body" > + <table id="user_list" class="table table-bordered table-striped" width="100%" > + <thead> + <tr> + <th name="id" >ID</th> + <th name="username" >${I18n.user_username}</th> + <th name="password" >${I18n.user_password}</th> + <th name="role" >${I18n.user_role}</th> + <th name="permission" >${I18n.user_permission}</th> + <th>${I18n.system_opt}</th> + </tr> + </thead> + <tbody></tbody> + <tfoot></tfoot> + </table> + </div> + </div> + </div> + </div> + </section> + </div> + + <!-- footer --> + <@netCommon.commonFooter /> +</div> + +<!-- 新增.模态框 --> +<div class="modal fade" id="addModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.user_add}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_username}<font color="red">*</font></label> + <div class="col-sm-8"><input type="text" class="form-control" name="username" placeholder="${I18n.system_please_input}${I18n.user_username}" maxlength="20" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_password}<font color="red">*</font></label> + <div class="col-sm-8"><input type="text" class="form-control" name="password" placeholder="${I18n.system_please_input}${I18n.user_password}" maxlength="20" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_role}<font color="red">*</font></label> + <div class="col-sm-10"> + <input type="radio" name="role" value="0" checked />${I18n.user_role_normal} + + <input type="radio" name="role" value="1" />${I18n.user_role_admin} + </div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_permission}<font color="black">*</font></label> + <div class="col-sm-10"> + <#if groupList?exists && groupList?size gt 0> + <#list groupList as item> + <input type="checkbox" name="permission" value="${item.id}" />${item.title}(${item.appname})<br> + </#list> + </#if> + </div> + </div> + + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + </div> + </div> + + </form> + </div> + </div> + </div> +</div> + +<!-- 更新.模态框 --> +<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" >${I18n.user_update}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal form" role="form" > + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_username}<font color="red">*</font></label> + <div class="col-sm-8"><input type="text" class="form-control" name="username" placeholder="${I18n.system_please_input}${I18n.user_username}" maxlength="20" readonly ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_password}<font color="red">*</font></label> + <div class="col-sm-8"><input type="text" class="form-control" name="password" placeholder="${I18n.user_password_update_placeholder}" maxlength="20" ></div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_role}<font color="red">*</font></label> + <div class="col-sm-10"> + <input type="radio" name="role" value="0" />${I18n.user_role_normal} + + <input type="radio" name="role" value="1" />${I18n.user_role_admin} + </div> + </div> + <div class="form-group"> + <label for="lastname" class="col-sm-2 control-label">${I18n.user_permission}<font color="black">*</font></label> + <div class="col-sm-10"> + <#if groupList?exists && groupList?size gt 0> + <#list groupList as item> + <input type="checkbox" name="permission" value="${item.id}" />${item.title}(${item.appname})<br> + </#list> + </#if> + </div> + </div> + + <hr> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-6"> + <button type="submit" class="btn btn-primary" >${I18n.system_save}</button> + <button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button> + <input type="hidden" name="id" > + </div> + </div> + + </form> + </div> + </div> + </div> +</div> + +<@netCommon.commonScript /> +<!-- DataTables --> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net/js/jquery.dataTables.min.js"></script> +<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script> +<script src="${request.contextPath}/static/js/user.index.1.js"></script> +</body> +</html> diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java new file mode 100644 index 0000000..0d7b98f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java @@ -0,0 +1,22 @@ +package com.xxl.job.admin.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class AbstractSpringMvcTest { + + @Autowired + private WebApplicationContext applicationContext; + protected MockMvc mockMvc; + + @BeforeEach + public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.applicationContext).build(); + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java new file mode 100644 index 0000000..49bf8ae --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java @@ -0,0 +1,50 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.service.LoginService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import javax.servlet.http.Cookie; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +public class JobInfoControllerTest extends AbstractSpringMvcTest { + private static Logger logger = LoggerFactory.getLogger(JobInfoControllerTest.class); + + private Cookie cookie; + + @BeforeEach + public void login() throws Exception { + MvcResult ret = mockMvc.perform( + post("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("userName", "admin") + .param("password", "123456") + ).andReturn(); + cookie = ret.getResponse().getCookie(LoginService.LOGIN_IDENTITY_KEY); + } + + @Test + public void testAdd() throws Exception { + MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>(); + parameters.add("jobGroup", "1"); + parameters.add("triggerStatus", "-1"); + + MvcResult ret = mockMvc.perform( + post("/jobinfo/pageList") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + //.content(paramsJson) + .params(parameters) + .cookie(cookie) + ).andReturn(); + + logger.info(ret.getResponse().getContentAsString()); + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java new file mode 100644 index 0000000..34fb9d4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java @@ -0,0 +1,40 @@ +package com.xxl.job.admin.core.util; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static com.xxl.job.admin.core.util.JacksonUtil.writeValueAsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JacksonUtilTest { + + @Test + public void shouldWriteValueAsString() { + //given + Map<String, String> map = new HashMap<>(); + map.put("aaa", "111"); + map.put("bbb", "222"); + + //when + String json = writeValueAsString(map); + + //then + assertEquals(json, "{\"aaa\":\"111\",\"bbb\":\"222\"}"); + } + + @Test + public void shouldReadValueAsObject() { + //given + String jsonString = "{\"aaa\":\"111\",\"bbb\":\"222\"}"; + + //when + Map result = JacksonUtil.readValue(jsonString, Map.class); + + //then + assertEquals(result.get("aaa"), "111"); + assertEquals(result.get("bbb"),"222"); + + } +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobGroupDaoTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobGroupDaoTest.java new file mode 100644 index 0000000..90b68a9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobGroupDaoTest.java @@ -0,0 +1,44 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class XxlJobGroupDaoTest { + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + + @Test + public void test(){ + List<XxlJobGroup> list = xxlJobGroupDao.findAll(); + + List<XxlJobGroup> list2 = xxlJobGroupDao.findByAddressType(0); + + XxlJobGroup group = new XxlJobGroup(); + group.setAppname("setAppName"); + group.setTitle("setTitle"); + group.setAddressType(0); + group.setAddressList("setAddressList"); + group.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.save(group); + + XxlJobGroup group2 = xxlJobGroupDao.load(group.getId()); + group2.setAppname("setAppName2"); + group2.setTitle("setTitle2"); + group2.setAddressType(2); + group2.setAddressList("setAddressList2"); + group2.setUpdateTime(new Date()); + + int ret2 = xxlJobGroupDao.update(group2); + + int ret3 = xxlJobGroupDao.remove(group.getId()); + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobInfoDaoTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobInfoDaoTest.java new file mode 100644 index 0000000..0cb7d53 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobInfoDaoTest.java @@ -0,0 +1,86 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class XxlJobInfoDaoTest { + private static Logger logger = LoggerFactory.getLogger(XxlJobInfoDaoTest.class); + + @Resource + private XxlJobInfoDao xxlJobInfoDao; + + @Test + public void pageList(){ + List<XxlJobInfo> list = xxlJobInfoDao.pageList(0, 20, 0, -1, null, null, null); + int list_count = xxlJobInfoDao.pageListCount(0, 20, 0, -1, null, null, null); + + logger.info("", list); + logger.info("", list_count); + + List<XxlJobInfo> list2 = xxlJobInfoDao.getJobsByGroup(1); + } + + @Test + public void save_load(){ + XxlJobInfo info = new XxlJobInfo(); + info.setJobGroup(1); + info.setJobDesc("desc"); + info.setAuthor("setAuthor"); + info.setAlarmEmail("setAlarmEmail"); + info.setScheduleType(ScheduleTypeEnum.FIX_RATE.name()); + info.setScheduleConf(String.valueOf(33)); + info.setMisfireStrategy(MisfireStrategyEnum.DO_NOTHING.name()); + info.setExecutorRouteStrategy("setExecutorRouteStrategy"); + info.setExecutorHandler("setExecutorHandler"); + info.setExecutorParam("setExecutorParam"); + info.setExecutorBlockStrategy("setExecutorBlockStrategy"); + info.setGlueType("setGlueType"); + info.setGlueSource("setGlueSource"); + info.setGlueRemark("setGlueRemark"); + info.setChildJobId("1"); + + info.setAddTime(new Date()); + info.setUpdateTime(new Date()); + info.setGlueUpdatetime(new Date()); + + int count = xxlJobInfoDao.save(info); + + XxlJobInfo info2 = xxlJobInfoDao.loadById(info.getId()); + info.setScheduleType(ScheduleTypeEnum.FIX_RATE.name()); + info.setScheduleConf(String.valueOf(44)); + info.setMisfireStrategy(MisfireStrategyEnum.FIRE_ONCE_NOW.name()); + info2.setJobDesc("desc2"); + info2.setAuthor("setAuthor2"); + info2.setAlarmEmail("setAlarmEmail2"); + info2.setExecutorRouteStrategy("setExecutorRouteStrategy2"); + info2.setExecutorHandler("setExecutorHandler2"); + info2.setExecutorParam("setExecutorParam2"); + info2.setExecutorBlockStrategy("setExecutorBlockStrategy2"); + info2.setGlueType("setGlueType2"); + info2.setGlueSource("setGlueSource2"); + info2.setGlueRemark("setGlueRemark2"); + info2.setGlueUpdatetime(new Date()); + info2.setChildJobId("1"); + + info2.setUpdateTime(new Date()); + int item2 = xxlJobInfoDao.update(info2); + + xxlJobInfoDao.delete(info2.getId()); + + List<XxlJobInfo> list2 = xxlJobInfoDao.getJobsByGroup(1); + + int ret3 = xxlJobInfoDao.findAllCount(); + + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogDaoTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogDaoTest.java new file mode 100644 index 0000000..c5888bb --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogDaoTest.java @@ -0,0 +1,52 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLog; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class XxlJobLogDaoTest { + + @Resource + private XxlJobLogDao xxlJobLogDao; + + @Test + public void test(){ + List<XxlJobLog> list = xxlJobLogDao.pageList(0, 10, 1, 1, null, null, 1); + int list_count = xxlJobLogDao.pageListCount(0, 10, 1, 1, null, null, 1); + + XxlJobLog log = new XxlJobLog(); + log.setJobGroup(1); + log.setJobId(1); + + long ret1 = xxlJobLogDao.save(log); + XxlJobLog dto = xxlJobLogDao.load(log.getId()); + + log.setTriggerTime(new Date()); + log.setTriggerCode(1); + log.setTriggerMsg("1"); + log.setExecutorAddress("1"); + log.setExecutorHandler("1"); + log.setExecutorParam("1"); + ret1 = xxlJobLogDao.updateTriggerInfo(log); + dto = xxlJobLogDao.load(log.getId()); + + + log.setHandleTime(new Date()); + log.setHandleCode(2); + log.setHandleMsg("2"); + ret1 = xxlJobLogDao.updateHandleInfo(log); + dto = xxlJobLogDao.load(log.getId()); + + + List<Long> ret4 = xxlJobLogDao.findClearLogIds(1, 1, new Date(), 100, 100); + + int ret2 = xxlJobLogDao.delete(log.getJobId()); + + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogGlueDaoTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogGlueDaoTest.java new file mode 100644 index 0000000..1e9eee3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogGlueDaoTest.java @@ -0,0 +1,36 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class XxlJobLogGlueDaoTest { + + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + + @Test + public void test(){ + XxlJobLogGlue logGlue = new XxlJobLogGlue(); + logGlue.setJobId(1); + logGlue.setGlueType("1"); + logGlue.setGlueSource("1"); + logGlue.setGlueRemark("1"); + + logGlue.setAddTime(new Date()); + logGlue.setUpdateTime(new Date()); + int ret = xxlJobLogGlueDao.save(logGlue); + + List<XxlJobLogGlue> list = xxlJobLogGlueDao.findByJobId(1); + + int ret2 = xxlJobLogGlueDao.removeOld(1, 1); + + int ret3 =xxlJobLogGlueDao.deleteByJobId(1); + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobRegistryDaoTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobRegistryDaoTest.java new file mode 100644 index 0000000..5c0a15d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobRegistryDaoTest.java @@ -0,0 +1,30 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobRegistry; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class XxlJobRegistryDaoTest { + + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + + @Test + public void test(){ + int ret = xxlJobRegistryDao.registryUpdate("g1", "k1", "v1", new Date()); + if (ret < 1) { + ret = xxlJobRegistryDao.registrySave("g1", "k1", "v1", new Date()); + } + + List<XxlJobRegistry> list = xxlJobRegistryDao.findAll(1, new Date()); + + int ret2 = xxlJobRegistryDao.removeDead(Arrays.asList(1)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java new file mode 100644 index 0000000..29079f1 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java @@ -0,0 +1,25 @@ +package com.xxl.job.admin.util; + +import com.xxl.job.admin.core.util.I18nUtil; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * email util test + * + * @author xuxueli 2017-12-22 17:16:23 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class I18nUtilTest { + private static Logger logger = LoggerFactory.getLogger(I18nUtilTest.class); + + @Test + public void test(){ + logger.info(I18nUtil.getString("admin_name")); + logger.info(I18nUtil.getMultString("admin_name", "admin_name_full")); + logger.info(I18nUtil.getMultString()); + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/adminbiz/AdminBizTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/adminbiz/AdminBizTest.java new file mode 100644 index 0000000..6fb89e7 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/adminbiz/AdminBizTest.java @@ -0,0 +1,75 @@ +package com.xxl.job.adminbiz; + +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.client.AdminBizClient; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobContext; +import com.xxl.job.core.enums.RegistryConfig; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * admin api test + * + * @author xuxueli 2017-07-28 22:14:52 + */ +public class AdminBizTest { + + // admin-client + private static String addressUrl = "http://127.0.0.1:8080/xxl-job-admin/"; + private static String accessToken = null; + + + @Test + public void callback() throws Exception { + AdminBiz adminBiz = new AdminBizClient(addressUrl, accessToken); + + HandleCallbackParam param = new HandleCallbackParam(); + param.setLogId(1); + param.setHandleCode(XxlJobContext.HANDLE_CODE_SUCCESS); + + List<HandleCallbackParam> callbackParamList = Arrays.asList(param); + + ReturnT<String> returnT = adminBiz.callback(callbackParamList); + + assertTrue(returnT.getCode() == ReturnT.SUCCESS_CODE); + } + + /** + * registry executor + * + * @throws Exception + */ + @Test + public void registry() throws Exception { + AdminBiz adminBiz = new AdminBizClient(addressUrl, accessToken); + + RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), "xxl-job-executor-example", "127.0.0.1:9999"); + ReturnT<String> returnT = adminBiz.registry(registryParam); + + assertTrue(returnT.getCode() == ReturnT.SUCCESS_CODE); + } + + /** + * registry executor remove + * + * @throws Exception + */ + @Test + public void registryRemove() throws Exception { + AdminBiz adminBiz = new AdminBizClient(addressUrl, accessToken); + + RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), "xxl-job-executor-example", "127.0.0.1:9999"); + ReturnT<String> returnT = adminBiz.registryRemove(registryParam); + + assertTrue(returnT.getCode() == ReturnT.SUCCESS_CODE); + + } + +} diff --git a/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/executorbiz/ExecutorBizTest.java b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/executorbiz/ExecutorBizTest.java new file mode 100644 index 0000000..4facb89 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/test/java/com/xxl/job/executorbiz/ExecutorBizTest.java @@ -0,0 +1,105 @@ +package com.xxl.job.executorbiz; + +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.client.ExecutorBizClient; +import com.xxl.job.core.biz.model.*; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * executor api test + * + * Created by xuxueli on 17/5/12. + */ +public class ExecutorBizTest { + + // admin-client + private static String addressUrl = "http://127.0.0.1:9999/"; + private static String accessToken = null; + + @Test + public void beat() throws Exception { + ExecutorBiz executorBiz = new ExecutorBizClient(addressUrl, accessToken); + // Act + final ReturnT<String> retval = executorBiz.beat(); + + // Assert result + Assertions.assertNotNull(retval); + Assertions.assertNull(((ReturnT<String>) retval).getContent()); + Assertions.assertEquals(200, retval.getCode()); + Assertions.assertNull(retval.getMsg()); + } + + @Test + public void idleBeat(){ + ExecutorBiz executorBiz = new ExecutorBizClient(addressUrl, accessToken); + + final int jobId = 0; + + // Act + final ReturnT<String> retval = executorBiz.idleBeat(new IdleBeatParam(jobId)); + + // Assert result + Assertions.assertNotNull(retval); + Assertions.assertNull(((ReturnT<String>) retval).getContent()); + Assertions.assertEquals(500, retval.getCode()); + Assertions.assertEquals("job thread is running or has trigger queue.", retval.getMsg()); + } + + @Test + public void run(){ + ExecutorBiz executorBiz = new ExecutorBizClient(addressUrl, accessToken); + + // trigger data + final TriggerParam triggerParam = new TriggerParam(); + triggerParam.setJobId(1); + triggerParam.setExecutorHandler("demoJobHandler"); + triggerParam.setExecutorParams(null); + triggerParam.setExecutorBlockStrategy(ExecutorBlockStrategyEnum.COVER_EARLY.name()); + triggerParam.setGlueType(GlueTypeEnum.BEAN.name()); + triggerParam.setGlueSource(null); + triggerParam.setGlueUpdatetime(System.currentTimeMillis()); + triggerParam.setLogId(1); + triggerParam.setLogDateTime(System.currentTimeMillis()); + + // Act + final ReturnT<String> retval = executorBiz.run(triggerParam); + + // Assert result + Assertions.assertNotNull(retval); + } + + @Test + public void kill(){ + ExecutorBiz executorBiz = new ExecutorBizClient(addressUrl, accessToken); + + final int jobId = 0; + + // Act + final ReturnT<String> retval = executorBiz.kill(new KillParam(jobId)); + + // Assert result + Assertions.assertNotNull(retval); + Assertions.assertNull(((ReturnT<String>) retval).getContent()); + Assertions.assertEquals(200, retval.getCode()); + Assertions.assertNull(retval.getMsg()); + } + + @Test + public void log(){ + ExecutorBiz executorBiz = new ExecutorBizClient(addressUrl, accessToken); + + final long logDateTim = 0L; + final long logId = 0; + final int fromLineNum = 0; + + // Act + final ReturnT<LogResult> retval = executorBiz.log(new LogParam(logDateTim, logId, fromLineNum)); + + // Assert result + Assertions.assertNotNull(retval); + } + +} diff --git a/xxl-job/xxl-job-core/pom.xml b/xxl-job/xxl-job-core/pom.xml new file mode 100644 index 0000000..7a03592 --- /dev/null +++ b/xxl-job/xxl-job-core/pom.xml @@ -0,0 +1,64 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job</artifactId> + <version>2.3.1</version> + </parent> + <artifactId>xxl-job-core</artifactId> + <packaging>jar</packaging> + + <name>${project.artifactId}</name> + <description>A distributed task scheduling framework.</description> + <url>https://www.xuxueli.com/</url> + + <dependencies> + + <!-- ********************** embed server: netty + gson ********************** --> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + <version>${netty-all.version}</version> + </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>${gson.version}</version> + </dependency> + + <!-- ********************** plugin ********************** --> + <!-- groovy-all --> + <dependency> + <groupId>org.codehaus.groovy</groupId> + <artifactId>groovy</artifactId> + <version>${groovy.version}</version> + </dependency> + + <!-- spring-context --> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + <version>${spring.version}</version> + <scope>provided</scope> + </dependency> + + <!-- ********************** base ********************** --> + <!-- slf4j --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4j-api.version}</version> + </dependency> + + <!-- javax.annotation-api --> + <dependency> + <groupId>javax.annotation</groupId> + <artifactId>javax.annotation-api</artifactId> + <version>${javax.annotation-api.version}</version> + <scope>provided</scope> + </dependency> + + </dependencies> + +</project> \ No newline at end of file diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java new file mode 100644 index 0000000..8d7b944 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java @@ -0,0 +1,48 @@ +package com.xxl.job.core.biz; + +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; + +import java.util.List; + +/** + * @author xuxueli 2017-07-27 21:52:49 + */ +public interface AdminBiz { + + + // ---------------------- callback ---------------------- + + /** + * callback + * + * @param callbackParamList + * @return + */ + public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList); + + + // ---------------------- registry ---------------------- + + /** + * registry + * + * @param registryParam + * @return + */ + public ReturnT<String> registry(RegistryParam registryParam); + + /** + * registry remove + * + * @param registryParam + * @return + */ + public ReturnT<String> registryRemove(RegistryParam registryParam); + + + // ---------------------- biz (custome) ---------------------- + // group、job ... manage + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java new file mode 100644 index 0000000..986358f --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java @@ -0,0 +1,45 @@ +package com.xxl.job.core.biz; + +import com.xxl.job.core.biz.model.*; + +/** + * Created by xuxueli on 17/3/1. + */ +public interface ExecutorBiz { + + /** + * beat + * @return + */ + public ReturnT<String> beat(); + + /** + * idle beat + * + * @param idleBeatParam + * @return + */ + public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam); + + /** + * run + * @param triggerParam + * @return + */ + public ReturnT<String> run(TriggerParam triggerParam); + + /** + * kill + * @param killParam + * @return + */ + public ReturnT<String> kill(KillParam killParam); + + /** + * log + * @param logParam + * @return + */ + public ReturnT<LogResult> log(LogParam logParam); + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java new file mode 100644 index 0000000..95fa560 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java @@ -0,0 +1,50 @@ +package com.xxl.job.core.biz.client; + +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.XxlJobRemotingUtil; + +import java.util.List; + +/** + * admin api test + * + * @author xuxueli 2017-07-28 22:14:52 + */ +public class AdminBizClient implements AdminBiz { + + public AdminBizClient() { + } + public AdminBizClient(String addressUrl, String accessToken) { + this.addressUrl = addressUrl; + this.accessToken = accessToken; + + // valid + if (!this.addressUrl.endsWith("/")) { + this.addressUrl = this.addressUrl + "/"; + } + } + + private String addressUrl ; + private String accessToken; + private int timeout = 3; + + + @Override + public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) { + return XxlJobRemotingUtil.postBody(addressUrl+"api/callback", accessToken, timeout, callbackParamList, String.class); + } + + @Override + public ReturnT<String> registry(RegistryParam registryParam) { + return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class); + } + + @Override + public ReturnT<String> registryRemove(RegistryParam registryParam) { + return XxlJobRemotingUtil.postBody(addressUrl + "api/registryRemove", accessToken, timeout, registryParam, String.class); + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java new file mode 100644 index 0000000..9f59430 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java @@ -0,0 +1,56 @@ +package com.xxl.job.core.biz.client; + +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.*; +import com.xxl.job.core.util.XxlJobRemotingUtil; + +/** + * admin api test + * + * @author xuxueli 2017-07-28 22:14:52 + */ +public class ExecutorBizClient implements ExecutorBiz { + + public ExecutorBizClient() { + } + public ExecutorBizClient(String addressUrl, String accessToken) { + this.addressUrl = addressUrl; + this.accessToken = accessToken; + + // valid + if (!this.addressUrl.endsWith("/")) { + this.addressUrl = this.addressUrl + "/"; + } + } + + private String addressUrl ; + private String accessToken; + private int timeout = 3; + + + @Override + public ReturnT<String> beat() { + return XxlJobRemotingUtil.postBody(addressUrl+"beat", accessToken, timeout, "", String.class); + } + + @Override + public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam){ + return XxlJobRemotingUtil.postBody(addressUrl+"idleBeat", accessToken, timeout, idleBeatParam, String.class); + } + + @Override + public ReturnT<String> run(TriggerParam triggerParam) { + return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class); + } + + @Override + public ReturnT<String> kill(KillParam killParam) { + return XxlJobRemotingUtil.postBody(addressUrl + "kill", accessToken, timeout, killParam, String.class); + } + + @Override + public ReturnT<LogResult> log(LogParam logParam) { + return XxlJobRemotingUtil.postBody(addressUrl + "log", accessToken, timeout, logParam, LogResult.class); + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java new file mode 100644 index 0000000..8bdf709 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java @@ -0,0 +1,172 @@ +package com.xxl.job.core.biz.impl; + +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.*; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.executor.XxlJobExecutor; +import com.xxl.job.core.glue.GlueFactory; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.impl.GlueJobHandler; +import com.xxl.job.core.handler.impl.ScriptJobHandler; +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.thread.JobThread; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * Created by xuxueli on 17/3/1. + */ +public class ExecutorBizImpl implements ExecutorBiz { + private static Logger logger = LoggerFactory.getLogger(ExecutorBizImpl.class); + + @Override + public ReturnT<String> beat() { + return ReturnT.SUCCESS; + } + + @Override + public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam) { + + // isRunningOrHasQueue + boolean isRunningOrHasQueue = false; + JobThread jobThread = XxlJobExecutor.loadJobThread(idleBeatParam.getJobId()); + if (jobThread != null && jobThread.isRunningOrHasQueue()) { + isRunningOrHasQueue = true; + } + + if (isRunningOrHasQueue) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "job thread is running or has trigger queue."); + } + return ReturnT.SUCCESS; + } + + @Override + public ReturnT<String> run(TriggerParam triggerParam) { + // load old:jobHandler + jobThread + JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId()); + IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null; + String removeOldReason = null; + + // valid:jobHandler + jobThread + GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType()); + if (GlueTypeEnum.BEAN == glueTypeEnum) { + + // new jobhandler + IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler()); + + // valid old jobThread + if (jobThread!=null && jobHandler != newJobHandler) { + // change handler, need kill old thread + removeOldReason = "change jobhandler or glue type, and terminate the old job thread."; + + jobThread = null; + jobHandler = null; + } + + // valid handler + if (jobHandler == null) { + jobHandler = newJobHandler; + if (jobHandler == null) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found."); + } + } + + } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) { + + // valid old jobThread + if (jobThread != null && + !(jobThread.getHandler() instanceof GlueJobHandler + && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) { + // change handler or gluesource updated, need kill old thread + removeOldReason = "change job source or glue type, and terminate the old job thread."; + + jobThread = null; + jobHandler = null; + } + + // valid handler + if (jobHandler == null) { + try { + IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource()); + jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage()); + } + } + } else if (glueTypeEnum!=null && glueTypeEnum.isScript()) { + + // valid old jobThread + if (jobThread != null && + !(jobThread.getHandler() instanceof ScriptJobHandler + && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) { + // change script or gluesource updated, need kill old thread + removeOldReason = "change job source or glue type, and terminate the old job thread."; + + jobThread = null; + jobHandler = null; + } + + // valid handler + if (jobHandler == null) { + jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType())); + } + } else { + return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid."); + } + + // executor block strategy + if (jobThread != null) { + ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null); + if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) { + // discard when running + if (jobThread.isRunningOrHasQueue()) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle()); + } + } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) { + // kill running jobThread + if (jobThread.isRunningOrHasQueue()) { + removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle(); + + jobThread = null; + } + } else { + // just queue trigger + } + } + + // replace thread (new or exists invalid) + if (jobThread == null) { + jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason); + } + + // push data to queue + ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam); + return pushResult; + } + + @Override + public ReturnT<String> kill(KillParam killParam) { + // kill handlerThread, and create new one + JobThread jobThread = XxlJobExecutor.loadJobThread(killParam.getJobId()); + if (jobThread != null) { + XxlJobExecutor.removeJobThread(killParam.getJobId(), "scheduling center kill job."); + return ReturnT.SUCCESS; + } + + return new ReturnT<String>(ReturnT.SUCCESS_CODE, "job thread already killed."); + } + + @Override + public ReturnT<LogResult> log(LogParam logParam) { + // log filename: logPath/yyyy-MM-dd/9999.log + String logFileName = XxlJobFileAppender.makeLogFileName(new Date(logParam.getLogDateTim()), logParam.getLogId()); + + LogResult logResult = XxlJobFileAppender.readLog(logFileName, logParam.getFromLineNum()); + return new ReturnT<LogResult>(logResult); + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java new file mode 100644 index 0000000..b88ae28 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java @@ -0,0 +1,67 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * Created by xuxueli on 17/3/2. + */ +public class HandleCallbackParam implements Serializable { + private static final long serialVersionUID = 42L; + + private long logId; + private long logDateTim; + + private int handleCode; + private String handleMsg; + + public HandleCallbackParam(){} + public HandleCallbackParam(long logId, long logDateTim, int handleCode, String handleMsg) { + this.logId = logId; + this.logDateTim = logDateTim; + this.handleCode = handleCode; + this.handleMsg = handleMsg; + } + + public long getLogId() { + return logId; + } + + public void setLogId(long logId) { + this.logId = logId; + } + + public long getLogDateTim() { + return logDateTim; + } + + public void setLogDateTim(long logDateTim) { + this.logDateTim = logDateTim; + } + + public int getHandleCode() { + return handleCode; + } + + public void setHandleCode(int handleCode) { + this.handleCode = handleCode; + } + + public String getHandleMsg() { + return handleMsg; + } + + public void setHandleMsg(String handleMsg) { + this.handleMsg = handleMsg; + } + + @Override + public String toString() { + return "HandleCallbackParam{" + + "logId=" + logId + + ", logDateTim=" + logDateTim + + ", handleCode=" + handleCode + + ", handleMsg='" + handleMsg + '\'' + + '}'; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java new file mode 100644 index 0000000..80cd288 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java @@ -0,0 +1,28 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * @author xuxueli 2020-04-11 22:27 + */ +public class IdleBeatParam implements Serializable { + private static final long serialVersionUID = 42L; + + public IdleBeatParam() { + } + public IdleBeatParam(int jobId) { + this.jobId = jobId; + } + + private int jobId; + + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java new file mode 100644 index 0000000..b0d96e3 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java @@ -0,0 +1,28 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * @author xuxueli 2020-04-11 22:27 + */ +public class KillParam implements Serializable { + private static final long serialVersionUID = 42L; + + public KillParam() { + } + public KillParam(int jobId) { + this.jobId = jobId; + } + + private int jobId; + + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java new file mode 100644 index 0000000..cdaecb9 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java @@ -0,0 +1,47 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * @author xuxueli 2020-04-11 22:27 + */ +public class LogParam implements Serializable { + private static final long serialVersionUID = 42L; + + public LogParam() { + } + public LogParam(long logDateTim, long logId, int fromLineNum) { + this.logDateTim = logDateTim; + this.logId = logId; + this.fromLineNum = fromLineNum; + } + + private long logDateTim; + private long logId; + private int fromLineNum; + + public long getLogDateTim() { + return logDateTim; + } + + public void setLogDateTim(long logDateTim) { + this.logDateTim = logDateTim; + } + + public long getLogId() { + return logId; + } + + public void setLogId(long logId) { + this.logId = logId; + } + + public int getFromLineNum() { + return fromLineNum; + } + + public void setFromLineNum(int fromLineNum) { + this.fromLineNum = fromLineNum; + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java new file mode 100644 index 0000000..1ffdf7c --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java @@ -0,0 +1,56 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * Created by xuxueli on 17/3/23. + */ +public class LogResult implements Serializable { + private static final long serialVersionUID = 42L; + + public LogResult() { + } + public LogResult(int fromLineNum, int toLineNum, String logContent, boolean isEnd) { + this.fromLineNum = fromLineNum; + this.toLineNum = toLineNum; + this.logContent = logContent; + this.isEnd = isEnd; + } + + private int fromLineNum; + private int toLineNum; + private String logContent; + private boolean isEnd; + + public int getFromLineNum() { + return fromLineNum; + } + + public void setFromLineNum(int fromLineNum) { + this.fromLineNum = fromLineNum; + } + + public int getToLineNum() { + return toLineNum; + } + + public void setToLineNum(int toLineNum) { + this.toLineNum = toLineNum; + } + + public String getLogContent() { + return logContent; + } + + public void setLogContent(String logContent) { + this.logContent = logContent; + } + + public boolean isEnd() { + return isEnd; + } + + public void setEnd(boolean end) { + isEnd = end; + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java new file mode 100644 index 0000000..8526c3e --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java @@ -0,0 +1,54 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * Created by xuxueli on 2017-05-10 20:22:42 + */ +public class RegistryParam implements Serializable { + private static final long serialVersionUID = 42L; + + private String registryGroup; + private String registryKey; + private String registryValue; + + public RegistryParam(){} + public RegistryParam(String registryGroup, String registryKey, String registryValue) { + this.registryGroup = registryGroup; + this.registryKey = registryKey; + this.registryValue = registryValue; + } + + public String getRegistryGroup() { + return registryGroup; + } + + public void setRegistryGroup(String registryGroup) { + this.registryGroup = registryGroup; + } + + public String getRegistryKey() { + return registryKey; + } + + public void setRegistryKey(String registryKey) { + this.registryKey = registryKey; + } + + public String getRegistryValue() { + return registryValue; + } + + public void setRegistryValue(String registryValue) { + this.registryValue = registryValue; + } + + @Override + public String toString() { + return "RegistryParam{" + + "registryGroup='" + registryGroup + '\'' + + ", registryKey='" + registryKey + '\'' + + ", registryValue='" + registryValue + '\'' + + '}'; + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java new file mode 100644 index 0000000..83d7a36 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java @@ -0,0 +1,57 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * common return + * @author xuxueli 2015-12-4 16:32:31 + * @param <T> + */ +public class ReturnT<T> implements Serializable { + public static final long serialVersionUID = 42L; + + public static final int SUCCESS_CODE = 200; + public static final int FAIL_CODE = 500; + + public static final ReturnT<String> SUCCESS = new ReturnT<String>(null); + public static final ReturnT<String> FAIL = new ReturnT<String>(FAIL_CODE, null); + + private int code; + private String msg; + private T content; + + public ReturnT(){} + public ReturnT(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ReturnT(T content) { + this.code = SUCCESS_CODE; + this.content = content; + } + + public int getCode() { + return code; + } + public void setCode(int code) { + this.code = code; + } + public String getMsg() { + return msg; + } + public void setMsg(String msg) { + this.msg = msg; + } + public T getContent() { + return content; + } + public void setContent(T content) { + this.content = content; + } + + @Override + public String toString() { + return "ReturnT [code=" + code + ", msg=" + msg + ", content=" + content + "]"; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java new file mode 100644 index 0000000..4f56368 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java @@ -0,0 +1,144 @@ +package com.xxl.job.core.biz.model; + +import java.io.Serializable; + +/** + * Created by xuxueli on 16/7/22. + */ +public class TriggerParam implements Serializable{ + private static final long serialVersionUID = 42L; + + private int jobId; + + private String executorHandler; + private String executorParams; + private String executorBlockStrategy; + private int executorTimeout; + + private long logId; + private long logDateTime; + + private String glueType; + private String glueSource; + private long glueUpdatetime; + + private int broadcastIndex; + private int broadcastTotal; + + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParams() { + return executorParams; + } + + public void setExecutorParams(String executorParams) { + this.executorParams = executorParams; + } + + public String getExecutorBlockStrategy() { + return executorBlockStrategy; + } + + public void setExecutorBlockStrategy(String executorBlockStrategy) { + this.executorBlockStrategy = executorBlockStrategy; + } + + public int getExecutorTimeout() { + return executorTimeout; + } + + public void setExecutorTimeout(int executorTimeout) { + this.executorTimeout = executorTimeout; + } + + public long getLogId() { + return logId; + } + + public void setLogId(long logId) { + this.logId = logId; + } + + public long getLogDateTime() { + return logDateTime; + } + + public void setLogDateTime(long logDateTime) { + this.logDateTime = logDateTime; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public long getGlueUpdatetime() { + return glueUpdatetime; + } + + public void setGlueUpdatetime(long glueUpdatetime) { + this.glueUpdatetime = glueUpdatetime; + } + + public int getBroadcastIndex() { + return broadcastIndex; + } + + public void setBroadcastIndex(int broadcastIndex) { + this.broadcastIndex = broadcastIndex; + } + + public int getBroadcastTotal() { + return broadcastTotal; + } + + public void setBroadcastTotal(int broadcastTotal) { + this.broadcastTotal = broadcastTotal; + } + + + @Override + public String toString() { + return "TriggerParam{" + + "jobId=" + jobId + + ", executorHandler='" + executorHandler + '\'' + + ", executorParams='" + executorParams + '\'' + + ", executorBlockStrategy='" + executorBlockStrategy + '\'' + + ", executorTimeout=" + executorTimeout + + ", logId=" + logId + + ", logDateTime=" + logDateTime + + ", glueType='" + glueType + '\'' + + ", glueSource='" + glueSource + '\'' + + ", glueUpdatetime=" + glueUpdatetime + + ", broadcastIndex=" + broadcastIndex + + ", broadcastTotal=" + broadcastTotal + + '}'; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java new file mode 100644 index 0000000..7e35012 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java @@ -0,0 +1,122 @@ +package com.xxl.job.core.context; + +/** + * xxl-job context + * + * @author xuxueli 2020-05-21 + * [Dear hj] + */ +public class XxlJobContext { + + public static final int HANDLE_CODE_SUCCESS = 200; + public static final int HANDLE_CODE_FAIL = 500; + public static final int HANDLE_CODE_TIMEOUT = 502; + + // ---------------------- base info ---------------------- + + /** + * job id + */ + private final long jobId; + + /** + * job param + */ + private final String jobParam; + + // ---------------------- for log ---------------------- + + /** + * job log filename + */ + private final String jobLogFileName; + + // ---------------------- for shard ---------------------- + + /** + * shard index + */ + private final int shardIndex; + + /** + * shard total + */ + private final int shardTotal; + + // ---------------------- for handle ---------------------- + + /** + * handleCode:The result status of job execution + * + * 200 : success + * 500 : fail + * 502 : timeout + * + */ + private int handleCode; + + /** + * handleMsg:The simple log msg of job execution + */ + private String handleMsg; + + + public XxlJobContext(long jobId, String jobParam, String jobLogFileName, int shardIndex, int shardTotal) { + this.jobId = jobId; + this.jobParam = jobParam; + this.jobLogFileName = jobLogFileName; + this.shardIndex = shardIndex; + this.shardTotal = shardTotal; + + this.handleCode = HANDLE_CODE_SUCCESS; // default success + } + + public long getJobId() { + return jobId; + } + + public String getJobParam() { + return jobParam; + } + + public String getJobLogFileName() { + return jobLogFileName; + } + + public int getShardIndex() { + return shardIndex; + } + + public int getShardTotal() { + return shardTotal; + } + + public void setHandleCode(int handleCode) { + this.handleCode = handleCode; + } + + public int getHandleCode() { + return handleCode; + } + + public void setHandleMsg(String handleMsg) { + this.handleMsg = handleMsg; + } + + public String getHandleMsg() { + return handleMsg; + } + + // ---------------------- tool ---------------------- + + private static InheritableThreadLocal<XxlJobContext> contextHolder = new InheritableThreadLocal<XxlJobContext>(); // support for child thread of job handler) + + public static void setXxlJobContext(XxlJobContext xxlJobContext){ + contextHolder.set(xxlJobContext); + } + + public static XxlJobContext getXxlJobContext(){ + return contextHolder.get(); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java new file mode 100644 index 0000000..eb20c18 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java @@ -0,0 +1,255 @@ +package com.xxl.job.core.context; + +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Date; + +/** + * helper for xxl-job + * + * @author xuxueli 2020-11-05 + */ +public class XxlJobHelper { + + // ---------------------- base info ---------------------- + + /** + * current JobId + * + * @return + */ + public static long getJobId() { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return -1; + } + + return xxlJobContext.getJobId(); + } + + /** + * current JobParam + * + * @return + */ + public static String getJobParam() { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return null; + } + + return xxlJobContext.getJobParam(); + } + + // ---------------------- for log ---------------------- + + /** + * current JobLogFileName + * + * @return + */ + public static String getJobLogFileName() { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return null; + } + + return xxlJobContext.getJobLogFileName(); + } + + // ---------------------- for shard ---------------------- + + /** + * current ShardIndex + * + * @return + */ + public static int getShardIndex() { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return -1; + } + + return xxlJobContext.getShardIndex(); + } + + /** + * current ShardTotal + * + * @return + */ + public static int getShardTotal() { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return -1; + } + + return xxlJobContext.getShardTotal(); + } + + // ---------------------- tool for log ---------------------- + + private static Logger logger = LoggerFactory.getLogger("xxl-job logger"); + + /** + * append log with pattern + * + * @param appendLogPattern like "aaa {} bbb {} ccc" + * @param appendLogArguments like "111, true" + */ + public static boolean log(String appendLogPattern, Object ... appendLogArguments) { + + FormattingTuple ft = MessageFormatter.arrayFormat(appendLogPattern, appendLogArguments); + String appendLog = ft.getMessage(); + + /*appendLog = appendLogPattern; + if (appendLogArguments!=null && appendLogArguments.length>0) { + appendLog = MessageFormat.format(appendLogPattern, appendLogArguments); + }*/ + + StackTraceElement callInfo = new Throwable().getStackTrace()[1]; + return logDetail(callInfo, appendLog); + } + + /** + * append exception stack + * + * @param e + */ + public static boolean log(Throwable e) { + + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + String appendLog = stringWriter.toString(); + + StackTraceElement callInfo = new Throwable().getStackTrace()[1]; + return logDetail(callInfo, appendLog); + } + + /** + * append log + * + * @param callInfo + * @param appendLog + */ + private static boolean logDetail(StackTraceElement callInfo, String appendLog) { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return false; + } + + /*// "yyyy-MM-dd HH:mm:ss [ClassName]-[MethodName]-[LineNumber]-[ThreadName] log"; + StackTraceElement[] stackTraceElements = new Throwable().getStackTrace(); + StackTraceElement callInfo = stackTraceElements[1];*/ + + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append(DateUtil.formatDateTime(new Date())).append(" ") + .append("["+ callInfo.getClassName() + "#" + callInfo.getMethodName() +"]").append("-") + .append("["+ callInfo.getLineNumber() +"]").append("-") + .append("["+ Thread.currentThread().getName() +"]").append(" ") + .append(appendLog!=null?appendLog:""); + String formatAppendLog = stringBuffer.toString(); + + // appendlog + String logFileName = xxlJobContext.getJobLogFileName(); + + if (logFileName!=null && logFileName.trim().length()>0) { + XxlJobFileAppender.appendLog(logFileName, formatAppendLog); + return true; + } else { + logger.info(">>>>>>>>>>> {}", formatAppendLog); + return false; + } + } + + // ---------------------- tool for handleResult ---------------------- + + /** + * handle success + * + * @return + */ + public static boolean handleSuccess(){ + return handleResult(XxlJobContext.HANDLE_CODE_SUCCESS, null); + } + + /** + * handle success with log msg + * + * @param handleMsg + * @return + */ + public static boolean handleSuccess(String handleMsg) { + return handleResult(XxlJobContext.HANDLE_CODE_SUCCESS, handleMsg); + } + + /** + * handle fail + * + * @return + */ + public static boolean handleFail(){ + return handleResult(XxlJobContext.HANDLE_CODE_FAIL, null); + } + + /** + * handle fail with log msg + * + * @param handleMsg + * @return + */ + public static boolean handleFail(String handleMsg) { + return handleResult(XxlJobContext.HANDLE_CODE_FAIL, handleMsg); + } + + /** + * handle timeout + * + * @return + */ + public static boolean handleTimeout(){ + return handleResult(XxlJobContext.HANDLE_CODE_TIMEOUT, null); + } + + /** + * handle timeout with log msg + * + * @param handleMsg + * @return + */ + public static boolean handleTimeout(String handleMsg){ + return handleResult(XxlJobContext.HANDLE_CODE_TIMEOUT, handleMsg); + } + + /** + * @param handleCode + * + * 200 : success + * 500 : fail + * 502 : timeout + * + * @param handleMsg + * @return + */ + public static boolean handleResult(int handleCode, String handleMsg) { + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext == null) { + return false; + } + + xxlJobContext.setHandleCode(handleCode); + if (handleMsg != null) { + xxlJobContext.setHandleMsg(handleMsg); + } + return true; + } + + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java new file mode 100644 index 0000000..a9dc1be --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java @@ -0,0 +1,35 @@ +package com.xxl.job.core.enums; + +/** + * Created by xuxueli on 17/5/9. + */ +public enum ExecutorBlockStrategyEnum { + + SERIAL_EXECUTION("Serial execution"), + /*CONCURRENT_EXECUTION("并行"),*/ + DISCARD_LATER("Discard Later"), + COVER_EARLY("Cover Early"); + + private String title; + private ExecutorBlockStrategyEnum (String title) { + this.title = title; + } + + public void setTitle(String title) { + this.title = title; + } + public String getTitle() { + return title; + } + + public static ExecutorBlockStrategyEnum match(String name, ExecutorBlockStrategyEnum defaultItem) { + if (name != null) { + for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + } + return defaultItem; + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java new file mode 100644 index 0000000..798beae --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java @@ -0,0 +1,13 @@ +package com.xxl.job.core.enums; + +/** + * Created by xuxueli on 17/5/10. + */ +public class RegistryConfig { + + public static final int BEAT_TIMEOUT = 30; + public static final int DEAD_TIMEOUT = BEAT_TIMEOUT * 3; + + public enum RegistType{ EXECUTOR, ADMIN } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java new file mode 100644 index 0000000..4719b7b --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java @@ -0,0 +1,271 @@ +package com.xxl.job.core.executor; + +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.client.AdminBizClient; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.annotation.XxlJob; +import com.xxl.job.core.handler.impl.MethodJobHandler; +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.server.EmbedServer; +import com.xxl.job.core.thread.JobLogFileCleanThread; +import com.xxl.job.core.thread.JobThread; +import com.xxl.job.core.thread.TriggerCallbackThread; +import com.xxl.job.core.util.IpUtil; +import com.xxl.job.core.util.NetUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Created by xuxueli on 2016/3/2 21:14. + */ +public class XxlJobExecutor { + private static final Logger logger = LoggerFactory.getLogger(XxlJobExecutor.class); + + // ---------------------- param ---------------------- + private String adminAddresses; + private String accessToken; + private String appname; + private String address; + private String ip; + private int port; + private String logPath; + private int logRetentionDays; + + public void setAdminAddresses(String adminAddresses) { + this.adminAddresses = adminAddresses; + } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + public void setAppname(String appname) { + this.appname = appname; + } + public void setAddress(String address) { + this.address = address; + } + public void setIp(String ip) { + this.ip = ip; + } + public void setPort(int port) { + this.port = port; + } + public void setLogPath(String logPath) { + this.logPath = logPath; + } + public void setLogRetentionDays(int logRetentionDays) { + this.logRetentionDays = logRetentionDays; + } + + + // ---------------------- start + stop ---------------------- + public void start() throws Exception { + + // init logpath + XxlJobFileAppender.initLogPath(logPath); + + // init invoker, admin-client + initAdminBizList(adminAddresses, accessToken); + + + // init JobLogFileCleanThread + JobLogFileCleanThread.getInstance().start(logRetentionDays); + + // init TriggerCallbackThread + TriggerCallbackThread.getInstance().start(); + + // init executor-server + initEmbedServer(address, ip, port, appname, accessToken); + } + + public void destroy(){ + // destroy executor-server + stopEmbedServer(); + + // destroy jobThreadRepository + if (jobThreadRepository.size() > 0) { + for (Map.Entry<Integer, JobThread> item: jobThreadRepository.entrySet()) { + JobThread oldJobThread = removeJobThread(item.getKey(), "web container destroy and kill the job."); + // wait for job thread push result to callback queue + if (oldJobThread != null) { + try { + oldJobThread.join(); + } catch (InterruptedException e) { + logger.error(">>>>>>>>>>> xxl-job, JobThread destroy(join) error, jobId:{}", item.getKey(), e); + } + } + } + jobThreadRepository.clear(); + } + jobHandlerRepository.clear(); + + + // destroy JobLogFileCleanThread + JobLogFileCleanThread.getInstance().toStop(); + + // destroy TriggerCallbackThread + TriggerCallbackThread.getInstance().toStop(); + + } + + + // ---------------------- admin-client (rpc invoker) ---------------------- + private static List<AdminBiz> adminBizList; + private void initAdminBizList(String adminAddresses, String accessToken) throws Exception { + if (adminAddresses!=null && adminAddresses.trim().length()>0) { + for (String address: adminAddresses.trim().split(",")) { + if (address!=null && address.trim().length()>0) { + + AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken); + + if (adminBizList == null) { + adminBizList = new ArrayList<AdminBiz>(); + } + adminBizList.add(adminBiz); + } + } + } + } + + public static List<AdminBiz> getAdminBizList(){ + return adminBizList; + } + + // ---------------------- executor-server (rpc provider) ---------------------- + private EmbedServer embedServer = null; + + private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception { + + // fill ip port + port = port>0?port: NetUtil.findAvailablePort(9999); + ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp(); + + // generate address + if (address==null || address.trim().length()==0) { + String ip_port_address = IpUtil.getIpPort(ip, port); // registry-address:default use address to registry , otherwise use ip:port if address is null + address = "http://{ip_port}/".replace("{ip_port}", ip_port_address); + } + + // accessToken + if (accessToken==null || accessToken.trim().length()==0) { + logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken."); + } + + // start + embedServer = new EmbedServer(); + embedServer.start(address, port, appname, accessToken); + } + + private void stopEmbedServer() { + // stop provider factory + if (embedServer != null) { + try { + embedServer.stop(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + } + + + // ---------------------- job handler repository ---------------------- + private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>(); + public static IJobHandler loadJobHandler(String name){ + return jobHandlerRepository.get(name); + } + public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){ + logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler); + return jobHandlerRepository.put(name, jobHandler); + } + protected void registJobHandler(XxlJob xxlJob, Object bean, Method executeMethod){ + if (xxlJob == null) { + return; + } + + String name = xxlJob.value(); + //make and simplify the variables since they'll be called several times later + Class<?> clazz = bean.getClass(); + String methodName = executeMethod.getName(); + if (name.trim().length() == 0) { + throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + clazz + "#" + methodName + "] ."); + } + if (loadJobHandler(name) != null) { + throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts."); + } + + // execute method + /*if (!(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(String.class))) { + throw new RuntimeException("xxl-job method-jobhandler param-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " + + "The correct method format like \" public ReturnT<String> execute(String param) \" ."); + } + if (!method.getReturnType().isAssignableFrom(ReturnT.class)) { + throw new RuntimeException("xxl-job method-jobhandler return-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " + + "The correct method format like \" public ReturnT<String> execute(String param) \" ."); + }*/ + + executeMethod.setAccessible(true); + + // init and destroy + Method initMethod = null; + Method destroyMethod = null; + + if (xxlJob.init().trim().length() > 0) { + try { + initMethod = clazz.getDeclaredMethod(xxlJob.init()); + initMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + clazz + "#" + methodName + "] ."); + } + } + if (xxlJob.destroy().trim().length() > 0) { + try { + destroyMethod = clazz.getDeclaredMethod(xxlJob.destroy()); + destroyMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + clazz + "#" + methodName + "] ."); + } + } + + // registry jobhandler + registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod)); + + } + + + // ---------------------- job thread repository ---------------------- + private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>(); + public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){ + JobThread newJobThread = new JobThread(jobId, handler); + newJobThread.start(); + logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler}); + + JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread); // putIfAbsent | oh my god, map's put method return the old value!!! + if (oldJobThread != null) { + oldJobThread.toStop(removeOldReason); + oldJobThread.interrupt(); + } + + return newJobThread; + } + + public static JobThread removeJobThread(int jobId, String removeOldReason){ + JobThread oldJobThread = jobThreadRepository.remove(jobId); + if (oldJobThread != null) { + oldJobThread.toStop(removeOldReason); + oldJobThread.interrupt(); + + return oldJobThread; + } + return null; + } + + public static JobThread loadJobThread(int jobId){ + return jobThreadRepository.get(jobId); + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java new file mode 100644 index 0000000..53efbb9 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java @@ -0,0 +1,75 @@ +package com.xxl.job.core.executor.impl; + +import com.xxl.job.core.executor.XxlJobExecutor; +import com.xxl.job.core.handler.annotation.XxlJob; +import com.xxl.job.core.handler.impl.MethodJobHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +/** + * xxl-job executor (for frameless) + * + * @author xuxueli 2020-11-05 + */ +public class XxlJobSimpleExecutor extends XxlJobExecutor { + private static final Logger logger = LoggerFactory.getLogger(XxlJobSimpleExecutor.class); + + + private List<Object> xxlJobBeanList = new ArrayList<>(); + public List<Object> getXxlJobBeanList() { + return xxlJobBeanList; + } + public void setXxlJobBeanList(List<Object> xxlJobBeanList) { + this.xxlJobBeanList = xxlJobBeanList; + } + + + @Override + public void start() { + + // init JobHandler Repository (for method) + initJobHandlerMethodRepository(xxlJobBeanList); + + // super start + try { + super.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void destroy() { + super.destroy(); + } + + + private void initJobHandlerMethodRepository(List<Object> xxlJobBeanList) { + if (xxlJobBeanList==null || xxlJobBeanList.size()==0) { + return; + } + + // init job handler from method + for (Object bean: xxlJobBeanList) { + // method + Method[] methods = bean.getClass().getDeclaredMethods(); + if (methods.length == 0) { + continue; + } + for (Method executeMethod : methods) { + XxlJob xxlJob = executeMethod.getAnnotation(XxlJob.class); + // registry + registJobHandler(xxlJob, bean, executeMethod); + } + + } + + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java new file mode 100644 index 0000000..3c2a67d --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java @@ -0,0 +1,126 @@ +package com.xxl.job.core.executor.impl; + +import com.xxl.job.core.executor.XxlJobExecutor; +import com.xxl.job.core.glue.GlueFactory; +import com.xxl.job.core.handler.annotation.XxlJob; +import com.xxl.job.core.handler.impl.MethodJobHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.annotation.AnnotatedElementUtils; + +import java.lang.reflect.Method; +import java.util.Map; + + +/** + * xxl-job executor (for spring) + * + * @author xuxueli 2018-11-01 09:24:52 + */ +public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean { + private static final Logger logger = LoggerFactory.getLogger(XxlJobSpringExecutor.class); + + + // start + @Override + public void afterSingletonsInstantiated() { + + // init JobHandler Repository + /*initJobHandlerRepository(applicationContext);*/ + + // init JobHandler Repository (for method) + initJobHandlerMethodRepository(applicationContext); + + // refresh GlueFactory + GlueFactory.refreshInstance(1); + + // super start + try { + super.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // destroy + @Override + public void destroy() { + super.destroy(); + } + + + /*private void initJobHandlerRepository(ApplicationContext applicationContext) { + if (applicationContext == null) { + return; + } + + // init job handler action + Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(JobHandler.class); + + if (serviceBeanMap != null && serviceBeanMap.size() > 0) { + for (Object serviceBean : serviceBeanMap.values()) { + if (serviceBean instanceof IJobHandler) { + String name = serviceBean.getClass().getAnnotation(JobHandler.class).value(); + IJobHandler handler = (IJobHandler) serviceBean; + if (loadJobHandler(name) != null) { + throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts."); + } + registJobHandler(name, handler); + } + } + } + }*/ + + private void initJobHandlerMethodRepository(ApplicationContext applicationContext) { + if (applicationContext == null) { + return; + } + // init job handler from method + String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true); + for (String beanDefinitionName : beanDefinitionNames) { + Object bean = applicationContext.getBean(beanDefinitionName); + + Map<Method, XxlJob> annotatedMethods = null; // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean + try { + annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(), + new MethodIntrospector.MetadataLookup<XxlJob>() { + @Override + public XxlJob inspect(Method method) { + return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class); + } + }); + } catch (Throwable ex) { + logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex); + } + if (annotatedMethods==null || annotatedMethods.isEmpty()) { + continue; + } + + for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) { + Method executeMethod = methodXxlJobEntry.getKey(); + XxlJob xxlJob = methodXxlJobEntry.getValue(); + // regist + registJobHandler(xxlJob, bean, executeMethod); + } + } + } + + // ---------------------- applicationContext ---------------------- + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + XxlJobSpringExecutor.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java new file mode 100644 index 0000000..79a83a9 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java @@ -0,0 +1,90 @@ +package com.xxl.job.core.glue; + +import com.xxl.job.core.glue.impl.SpringGlueFactory; +import com.xxl.job.core.handler.IJobHandler; +import groovy.lang.GroovyClassLoader; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * glue factory, product class/object by name + * + * @author xuxueli 2016-1-2 20:02:27 + */ +public class GlueFactory { + + + private static GlueFactory glueFactory = new GlueFactory(); + public static GlueFactory getInstance(){ + return glueFactory; + } + public static void refreshInstance(int type){ + if (type == 0) { + glueFactory = new GlueFactory(); + } else if (type == 1) { + glueFactory = new SpringGlueFactory(); + } + } + + + /** + * groovy class loader + */ + private GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); + private ConcurrentMap<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>(); + + /** + * load new instance, prototype + * + * @param codeSource + * @return + * @throws Exception + */ + public IJobHandler loadNewInstance(String codeSource) throws Exception{ + if (codeSource!=null && codeSource.trim().length()>0) { + Class<?> clazz = getCodeSourceClass(codeSource); + if (clazz != null) { + Object instance = clazz.newInstance(); + if (instance!=null) { + if (instance instanceof IJobHandler) { + this.injectService(instance); + return (IJobHandler) instance; + } else { + throw new IllegalArgumentException(">>>>>>>>>>> xxl-glue, loadNewInstance error, " + + "cannot convert from instance["+ instance.getClass() +"] to IJobHandler"); + } + } + } + } + throw new IllegalArgumentException(">>>>>>>>>>> xxl-glue, loadNewInstance error, instance is null"); + } + private Class<?> getCodeSourceClass(String codeSource){ + try { + // md5 + byte[] md5 = MessageDigest.getInstance("MD5").digest(codeSource.getBytes()); + String md5Str = new BigInteger(1, md5).toString(16); + + Class<?> clazz = CLASS_CACHE.get(md5Str); + if(clazz == null){ + clazz = groovyClassLoader.parseClass(codeSource); + CLASS_CACHE.putIfAbsent(md5Str, clazz); + } + return clazz; + } catch (Exception e) { + return groovyClassLoader.parseClass(codeSource); + } + } + + /** + * inject service of bean field + * + * @param instance + */ + public void injectService(Object instance) { + // do something + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java new file mode 100644 index 0000000..a5c835c --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java @@ -0,0 +1,53 @@ +package com.xxl.job.core.glue; + +/** + * Created by xuxueli on 17/4/26. + */ +public enum GlueTypeEnum { + + BEAN("BEAN", false, null, null), + GLUE_GROOVY("GLUE(Java)", false, null, null), + GLUE_SHELL("GLUE(Shell)", true, "bash", ".sh"), + GLUE_PYTHON("GLUE(Python)", true, "python", ".py"), + GLUE_PHP("GLUE(PHP)", true, "php", ".php"), + GLUE_NODEJS("GLUE(Nodejs)", true, "node", ".js"), + GLUE_POWERSHELL("GLUE(PowerShell)", true, "powershell", ".ps1"); + + private String desc; + private boolean isScript; + private String cmd; + private String suffix; + + private GlueTypeEnum(String desc, boolean isScript, String cmd, String suffix) { + this.desc = desc; + this.isScript = isScript; + this.cmd = cmd; + this.suffix = suffix; + } + + public String getDesc() { + return desc; + } + + public boolean isScript() { + return isScript; + } + + public String getCmd() { + return cmd; + } + + public String getSuffix() { + return suffix; + } + + public static GlueTypeEnum match(String name){ + for (GlueTypeEnum item: GlueTypeEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return null; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java new file mode 100644 index 0000000..37e44d5 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java @@ -0,0 +1,80 @@ +package com.xxl.job.core.glue.impl; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import com.xxl.job.core.glue.GlueFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.AnnotationUtils; + +import javax.annotation.Resource; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +/** + * @author xuxueli 2018-11-01 + */ +public class SpringGlueFactory extends GlueFactory { + private static Logger logger = LoggerFactory.getLogger(SpringGlueFactory.class); + + + /** + * inject action of spring + * @param instance + */ + @Override + public void injectService(Object instance){ + if (instance==null) { + return; + } + + if (XxlJobSpringExecutor.getApplicationContext() == null) { + return; + } + + Field[] fields = instance.getClass().getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + + Object fieldBean = null; + // with bean-id, bean could be found by both @Resource and @Autowired, or bean could only be found by @Autowired + + if (AnnotationUtils.getAnnotation(field, Resource.class) != null) { + try { + Resource resource = AnnotationUtils.getAnnotation(field, Resource.class); + if (resource.name()!=null && resource.name().length()>0){ + fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(resource.name()); + } else { + fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getName()); + } + } catch (Exception e) { + } + if (fieldBean==null ) { + fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getType()); + } + } else if (AnnotationUtils.getAnnotation(field, Autowired.class) != null) { + Qualifier qualifier = AnnotationUtils.getAnnotation(field, Qualifier.class); + if (qualifier!=null && qualifier.value()!=null && qualifier.value().length()>0) { + fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(qualifier.value()); + } else { + fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getType()); + } + } + + if (fieldBean!=null) { + field.setAccessible(true); + try { + field.set(instance, fieldBean); + } catch (IllegalArgumentException e) { + logger.error(e.getMessage(), e); + } catch (IllegalAccessException e) { + logger.error(e.getMessage(), e); + } + } + } + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java new file mode 100644 index 0000000..9ff43f9 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java @@ -0,0 +1,38 @@ +package com.xxl.job.core.handler; + +/** + * job handler + * + * @author xuxueli 2015-12-19 19:06:38 + */ +public abstract class IJobHandler { + + + /** + * execute handler, invoked when executor receives a scheduling request + * + * @throws Exception + */ + public abstract void execute() throws Exception; + + + /*@Deprecated + public abstract ReturnT<String> execute(String param) throws Exception;*/ + + /** + * init handler, invoked when JobThread init + */ + public void init() throws Exception { + // do something + } + + + /** + * destroy handler, invoked when JobThread destroy + */ + public void destroy() throws Exception { + // do something + } + + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java new file mode 100644 index 0000000..dbdd4dc --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java @@ -0,0 +1,24 @@ +//package com.xxl.job.core.handler.annotation; +// +//import java.lang.annotation.ElementType; +//import java.lang.annotation.Inherited; +//import java.lang.annotation.Retention; +//import java.lang.annotation.RetentionPolicy; +//import java.lang.annotation.Target; +// +///** +// * annotation for job handler +// * +// * will be replaced by {@link com.xxl.job.core.handler.annotation.XxlJob} +// * +// * @author 2016-5-17 21:06:49 +// */ +//@Target({ElementType.TYPE}) +//@Retention(RetentionPolicy.RUNTIME) +//@Inherited +//@Deprecated +//public @interface JobHandler { +// +// String value(); +// +//} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java new file mode 100644 index 0000000..33fb675 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java @@ -0,0 +1,30 @@ +package com.xxl.job.core.handler.annotation; + +import java.lang.annotation.*; + +/** + * annotation for method jobhandler + * + * @author xuxueli 2019-12-11 20:50:13 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface XxlJob { + + /** + * jobhandler name + */ + String value(); + + /** + * init handler, invoked when JobThread init + */ + String init() default ""; + + /** + * destroy handler, invoked when JobThread destroy + */ + String destroy() default ""; + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java new file mode 100644 index 0000000..79b2d73 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java @@ -0,0 +1,38 @@ +package com.xxl.job.core.handler.impl; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.IJobHandler; + +/** + * glue job handler + * + * @author xuxueli 2016-5-19 21:05:45 + */ +public class GlueJobHandler extends IJobHandler { + + private long glueUpdatetime; + private IJobHandler jobHandler; + public GlueJobHandler(IJobHandler jobHandler, long glueUpdatetime) { + this.jobHandler = jobHandler; + this.glueUpdatetime = glueUpdatetime; + } + public long getGlueUpdatetime() { + return glueUpdatetime; + } + + @Override + public void execute() throws Exception { + XxlJobHelper.log("----------- glue.version:"+ glueUpdatetime +" -----------"); + jobHandler.execute(); + } + + @Override + public void init() throws Exception { + this.jobHandler.init(); + } + + @Override + public void destroy() throws Exception { + this.jobHandler.destroy(); + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java new file mode 100644 index 0000000..2912638 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java @@ -0,0 +1,53 @@ +package com.xxl.job.core.handler.impl; + +import com.xxl.job.core.handler.IJobHandler; + +import java.lang.reflect.Method; + +/** + * @author xuxueli 2019-12-11 21:12:18 + */ +public class MethodJobHandler extends IJobHandler { + + private final Object target; + private final Method method; + private Method initMethod; + private Method destroyMethod; + + public MethodJobHandler(Object target, Method method, Method initMethod, Method destroyMethod) { + this.target = target; + this.method = method; + + this.initMethod = initMethod; + this.destroyMethod = destroyMethod; + } + + @Override + public void execute() throws Exception { + Class<?>[] paramTypes = method.getParameterTypes(); + if (paramTypes.length > 0) { + method.invoke(target, new Object[paramTypes.length]); // method-param can not be primitive-types + } else { + method.invoke(target); + } + } + + @Override + public void init() throws Exception { + if(initMethod != null) { + initMethod.invoke(target); + } + } + + @Override + public void destroy() throws Exception { + if(destroyMethod != null) { + destroyMethod.invoke(target); + } + } + + @Override + public String toString() { + return super.toString()+"["+ target.getClass() + "#" + method.getName() +"]"; + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java new file mode 100644 index 0000000..7e0cee4 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java @@ -0,0 +1,93 @@ +package com.xxl.job.core.handler.impl; + +import com.xxl.job.core.context.XxlJobContext; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.util.ScriptUtil; + +import java.io.File; + +/** + * Created by xuxueli on 17/4/27. + */ +public class ScriptJobHandler extends IJobHandler { + + private int jobId; + private long glueUpdatetime; + private String gluesource; + private GlueTypeEnum glueType; + + public ScriptJobHandler(int jobId, long glueUpdatetime, String gluesource, GlueTypeEnum glueType){ + this.jobId = jobId; + this.glueUpdatetime = glueUpdatetime; + this.gluesource = gluesource; + this.glueType = glueType; + + // clean old script file + File glueSrcPath = new File(XxlJobFileAppender.getGlueSrcPath()); + if (glueSrcPath.exists()) { + File[] glueSrcFileList = glueSrcPath.listFiles(); + if (glueSrcFileList!=null && glueSrcFileList.length>0) { + for (File glueSrcFileItem : glueSrcFileList) { + if (glueSrcFileItem.getName().startsWith(String.valueOf(jobId)+"_")) { + glueSrcFileItem.delete(); + } + } + } + } + + } + + public long getGlueUpdatetime() { + return glueUpdatetime; + } + + @Override + public void execute() throws Exception { + + if (!glueType.isScript()) { + XxlJobHelper.handleFail("glueType["+ glueType +"] invalid."); + return; + } + + // cmd + String cmd = glueType.getCmd(); + + // make script file + String scriptFileName = XxlJobFileAppender.getGlueSrcPath() + .concat(File.separator) + .concat(String.valueOf(jobId)) + .concat("_") + .concat(String.valueOf(glueUpdatetime)) + .concat(glueType.getSuffix()); + File scriptFile = new File(scriptFileName); + if (!scriptFile.exists()) { + ScriptUtil.markScriptFile(scriptFileName, gluesource); + } + + // log file + String logFileName = XxlJobContext.getXxlJobContext().getJobLogFileName(); + + // script params:0=param、1=分片序号、2=分片总数 + String[] scriptParams = new String[3]; + scriptParams[0] = XxlJobHelper.getJobParam(); + scriptParams[1] = String.valueOf(XxlJobContext.getXxlJobContext().getShardIndex()); + scriptParams[2] = String.valueOf(XxlJobContext.getXxlJobContext().getShardTotal()); + + // invoke + XxlJobHelper.log("----------- script file:"+ scriptFileName +" -----------"); + int exitValue = ScriptUtil.execToFile(cmd, scriptFileName, logFileName, scriptParams); + + if (exitValue == 0) { + XxlJobHelper.handleSuccess(); + return; + } else { + XxlJobHelper.handleFail("script exit value("+exitValue+") is failed"); + return ; + } + + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java new file mode 100644 index 0000000..ff0585b --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java @@ -0,0 +1,220 @@ +package com.xxl.job.core.log; + +import com.xxl.job.core.biz.model.LogResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * store trigger log in each log-file + * @author xuxueli 2016-3-12 19:25:12 + */ +public class XxlJobFileAppender { + private static Logger logger = LoggerFactory.getLogger(XxlJobFileAppender.class); + + /** + * log base path + * + * strut like: + * ---/ + * ---/gluesource/ + * ---/gluesource/10_1514171108000.js + * ---/gluesource/10_1514171108000.js + * ---/2017-12-25/ + * ---/2017-12-25/639.log + * ---/2017-12-25/821.log + * + */ + private static String logBasePath = "/data/applogs/xxl-job/jobhandler"; + private static String glueSrcPath = logBasePath.concat("/gluesource"); + public static void initLogPath(String logPath){ + // init + if (logPath!=null && logPath.trim().length()>0) { + logBasePath = logPath; + } + // mk base dir + File logPathDir = new File(logBasePath); + if (!logPathDir.exists()) { + logPathDir.mkdirs(); + } + logBasePath = logPathDir.getPath(); + + // mk glue dir + File glueBaseDir = new File(logPathDir, "gluesource"); + if (!glueBaseDir.exists()) { + glueBaseDir.mkdirs(); + } + glueSrcPath = glueBaseDir.getPath(); + } + public static String getLogPath() { + return logBasePath; + } + public static String getGlueSrcPath() { + return glueSrcPath; + } + + /** + * log filename, like "logPath/yyyy-MM-dd/9999.log" + * + * @param triggerDate + * @param logId + * @return + */ + public static String makeLogFileName(Date triggerDate, long logId) { + + // filePath/yyyy-MM-dd + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // avoid concurrent problem, can not be static + File logFilePath = new File(getLogPath(), sdf.format(triggerDate)); + if (!logFilePath.exists()) { + logFilePath.mkdir(); + } + + // filePath/yyyy-MM-dd/9999.log + String logFileName = logFilePath.getPath() + .concat(File.separator) + .concat(String.valueOf(logId)) + .concat(".log"); + return logFileName; + } + + /** + * append log + * + * @param logFileName + * @param appendLog + */ + public static void appendLog(String logFileName, String appendLog) { + + // log file + if (logFileName==null || logFileName.trim().length()==0) { + return; + } + File logFile = new File(logFileName); + + if (!logFile.exists()) { + try { + logFile.createNewFile(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + return; + } + } + + // log + if (appendLog == null) { + appendLog = ""; + } + appendLog += "\r\n"; + + // append file content + FileOutputStream fos = null; + try { + fos = new FileOutputStream(logFile, true); + fos.write(appendLog.getBytes("utf-8")); + fos.flush(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + + } + + /** + * support read log-file + * + * @param logFileName + * @return log content + */ + public static LogResult readLog(String logFileName, int fromLineNum){ + + // valid log file + if (logFileName==null || logFileName.trim().length()==0) { + return new LogResult(fromLineNum, 0, "readLog fail, logFile not found", true); + } + File logFile = new File(logFileName); + + if (!logFile.exists()) { + return new LogResult(fromLineNum, 0, "readLog fail, logFile not exists", true); + } + + // read file + StringBuffer logContentBuffer = new StringBuffer(); + int toLineNum = 0; + LineNumberReader reader = null; + try { + //reader = new LineNumberReader(new FileReader(logFile)); + reader = new LineNumberReader(new InputStreamReader(new FileInputStream(logFile), "utf-8")); + String line = null; + + while ((line = reader.readLine())!=null) { + toLineNum = reader.getLineNumber(); // [from, to], start as 1 + if (toLineNum >= fromLineNum) { + logContentBuffer.append(line).append("\n"); + } + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + + // result + LogResult logResult = new LogResult(fromLineNum, toLineNum, logContentBuffer.toString(), false); + return logResult; + + /* + // it will return the number of characters actually skipped + reader.skip(Long.MAX_VALUE); + int maxLineNum = reader.getLineNumber(); + maxLineNum++; // 最大行号 + */ + } + + /** + * read log data + * @param logFile + * @return log line content + */ + public static String readLines(File logFile){ + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(logFile), "utf-8")); + if (reader != null) { + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + return null; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java new file mode 100644 index 0000000..540e0ea --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java @@ -0,0 +1,256 @@ +package com.xxl.job.core.server; + +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.impl.ExecutorBizImpl; +import com.xxl.job.core.biz.model.*; +import com.xxl.job.core.thread.ExecutorRegistryThread; +import com.xxl.job.core.util.GsonTool; +import com.xxl.job.core.util.ThrowableUtil; +import com.xxl.job.core.util.XxlJobRemotingUtil; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.CharsetUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; + +/** + * Copy from : https://github.com/xuxueli/xxl-rpc + * + * @author xuxueli 2020-04-11 21:25 + */ +public class EmbedServer { + private static final Logger logger = LoggerFactory.getLogger(EmbedServer.class); + + private ExecutorBiz executorBiz; + private Thread thread; + + public void start(final String address, final int port, final String appname, final String accessToken) { + executorBiz = new ExecutorBizImpl(); + thread = new Thread(new Runnable() { + @Override + public void run() { + // param + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor( + 0, + 200, + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, EmbedServer bizThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!"); + } + }); + try { + // start server + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer<SocketChannel>() { + @Override + public void initChannel(SocketChannel channel) throws Exception { + channel.pipeline() + .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle + .addLast(new HttpServerCodec()) + .addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL + .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool)); + } + }) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + // bind + ChannelFuture future = bootstrap.bind(port).sync(); + + logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port); + + // start registry + startRegistry(appname, address); + + // wait util stop + future.channel().closeFuture().sync(); + + } catch (InterruptedException e) { + logger.info(">>>>>>>>>>> xxl-job remoting server stop."); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job remoting server error.", e); + } finally { + // stop + try { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + } + }); + thread.setDaemon(true); // daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave + thread.start(); + } + + public void stop() throws Exception { + // destroy server thread + if (thread != null && thread.isAlive()) { + thread.interrupt(); + } + + // stop registry + stopRegistry(); + logger.info(">>>>>>>>>>> xxl-job remoting server destroy success."); + } + + + // ---------------------- registry ---------------------- + + /** + * netty_http + * <p> + * Copy from : https://github.com/xuxueli/xxl-rpc + * + * @author xuxueli 2015-11-24 22:25:15 + */ + public static class EmbedHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { + private static final Logger logger = LoggerFactory.getLogger(EmbedHttpServerHandler.class); + + private ExecutorBiz executorBiz; + private String accessToken; + private ThreadPoolExecutor bizThreadPool; + + public EmbedHttpServerHandler(ExecutorBiz executorBiz, String accessToken, ThreadPoolExecutor bizThreadPool) { + this.executorBiz = executorBiz; + this.accessToken = accessToken; + this.bizThreadPool = bizThreadPool; + } + + @Override + protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { + // request parse + //final byte[] requestBytes = ByteBufUtil.getBytes(msg.content()); // byteBuf.toString(io.netty.util.CharsetUtil.UTF_8); + String requestData = msg.content().toString(CharsetUtil.UTF_8); + String uri = msg.uri(); + HttpMethod httpMethod = msg.method(); + boolean keepAlive = HttpUtil.isKeepAlive(msg); + String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN); + + // invoke + bizThreadPool.execute(new Runnable() { + @Override + public void run() { + // do invoke + Object responseObj = process(httpMethod, uri, requestData, accessTokenReq); + + // to json + String responseJson = GsonTool.toJson(responseObj); + + // write response + writeResponse(ctx, keepAlive, responseJson); + } + }); + } + + private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) { + // valid + if (HttpMethod.POST != httpMethod) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support."); + } + if (uri == null || uri.trim().length() == 0) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty."); + } + if (accessToken != null + && accessToken.trim().length() > 0 + && !accessToken.equals(accessTokenReq)) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong."); + } + + // services mapping + try { + switch (uri) { + case "/beat": + return executorBiz.beat(); + case "/idleBeat": + IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class); + return executorBiz.idleBeat(idleBeatParam); + case "/run": + TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class); + return executorBiz.run(triggerParam); + case "/kill": + KillParam killParam = GsonTool.fromJson(requestData, KillParam.class); + return executorBiz.kill(killParam); + case "/log": + LogParam logParam = GsonTool.fromJson(requestData, LogParam.class); + return executorBiz.log(logParam); + default: + return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping(" + uri + ") not found."); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e)); + } + } + + /** + * write response + */ + private void writeResponse(ChannelHandlerContext ctx, boolean keepAlive, String responseJson) { + // write response + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8)); // Unpooled.wrappedBuffer(responseJson) + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); // HttpHeaderValues.TEXT_PLAIN.toString() + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + if (keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + ctx.writeAndFlush(response); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.error(">>>>>>>>>>> xxl-job provider netty_http server caught exception", cause); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + ctx.channel().close(); // beat 3N, close if idle + logger.debug(">>>>>>>>>>> xxl-job provider netty_http server close an idle channel."); + } else { + super.userEventTriggered(ctx, evt); + } + } + } + + // ---------------------- registry ---------------------- + + public void startRegistry(final String appname, final String address) { + // start registry + ExecutorRegistryThread.getInstance().start(appname, address); + } + + public void stopRegistry() { + // stop registry + ExecutorRegistryThread.getInstance().toStop(); + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java new file mode 100644 index 0000000..e43a2a4 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java @@ -0,0 +1,129 @@ +package com.xxl.job.core.thread; + +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import com.xxl.job.core.executor.XxlJobExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * Created by xuxueli on 17/3/2. + */ +public class ExecutorRegistryThread { + private static Logger logger = LoggerFactory.getLogger(ExecutorRegistryThread.class); + + private static ExecutorRegistryThread instance = new ExecutorRegistryThread(); + public static ExecutorRegistryThread getInstance(){ + return instance; + } + + private Thread registryThread; + private volatile boolean toStop = false; + public void start(final String appname, final String address){ + + // valid + if (appname==null || appname.trim().length()==0) { + logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, appname is null."); + return; + } + if (XxlJobExecutor.getAdminBizList() == null) { + logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, adminAddresses is null."); + return; + } + + registryThread = new Thread(new Runnable() { + @Override + public void run() { + + // registry + while (!toStop) { + try { + RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address); + for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { + try { + ReturnT<String> registryResult = adminBiz.registry(registryParam); + if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) { + registryResult = ReturnT.SUCCESS; + logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult}); + break; + } else { + logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult}); + } + } catch (Exception e) { + logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e); + } + + } + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + + } + + try { + if (!toStop) { + TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); + } + } catch (InterruptedException e) { + if (!toStop) { + logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage()); + } + } + } + + // registry remove + try { + RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address); + for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { + try { + ReturnT<String> registryResult = adminBiz.registryRemove(registryParam); + if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) { + registryResult = ReturnT.SUCCESS; + logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult}); + break; + } else { + logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult}); + } + } catch (Exception e) { + if (!toStop) { + logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e); + } + + } + + } + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + logger.info(">>>>>>>>>>> xxl-job, executor registry thread destroy."); + + } + }); + registryThread.setDaemon(true); + registryThread.setName("xxl-job, executor ExecutorRegistryThread"); + registryThread.start(); + } + + public void toStop() { + toStop = true; + + // interrupt and wait + if (registryThread != null) { + registryThread.interrupt(); + try { + registryThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java new file mode 100644 index 0000000..b569154 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java @@ -0,0 +1,124 @@ +package com.xxl.job.core.thread; + +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.util.FileUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * job file clean thread + * + * @author xuxueli 2017-12-29 16:23:43 + */ +public class JobLogFileCleanThread { + private static Logger logger = LoggerFactory.getLogger(JobLogFileCleanThread.class); + + private static JobLogFileCleanThread instance = new JobLogFileCleanThread(); + public static JobLogFileCleanThread getInstance(){ + return instance; + } + + private Thread localThread; + private volatile boolean toStop = false; + public void start(final long logRetentionDays){ + + // limit min value + if (logRetentionDays < 3 ) { + return; + } + + localThread = new Thread(new Runnable() { + @Override + public void run() { + while (!toStop) { + try { + // clean log dir, over logRetentionDays + File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles(); + if (childDirs!=null && childDirs.length>0) { + + // today + Calendar todayCal = Calendar.getInstance(); + todayCal.set(Calendar.HOUR_OF_DAY,0); + todayCal.set(Calendar.MINUTE,0); + todayCal.set(Calendar.SECOND,0); + todayCal.set(Calendar.MILLISECOND,0); + + Date todayDate = todayCal.getTime(); + + for (File childFile: childDirs) { + + // valid + if (!childFile.isDirectory()) { + continue; + } + if (childFile.getName().indexOf("-") == -1) { + continue; + } + + // file create date + Date logFileCreateDate = null; + try { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + logFileCreateDate = simpleDateFormat.parse(childFile.getName()); + } catch (ParseException e) { + logger.error(e.getMessage(), e); + } + if (logFileCreateDate == null) { + continue; + } + + if ((todayDate.getTime()-logFileCreateDate.getTime()) >= logRetentionDays * (24 * 60 * 60 * 1000) ) { + FileUtil.deleteRecursively(childFile); + } + + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + + } + + try { + TimeUnit.DAYS.sleep(1); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destroy."); + + } + }); + localThread.setDaemon(true); + localThread.setName("xxl-job, executor JobLogFileCleanThread"); + localThread.start(); + } + + public void toStop() { + toStop = true; + + if (localThread == null) { + return; + } + + // interrupt and wait + localThread.interrupt(); + try { + localThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java new file mode 100644 index 0000000..cf07a55 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java @@ -0,0 +1,252 @@ +package com.xxl.job.core.thread; + +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import com.xxl.job.core.context.XxlJobContext; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.executor.XxlJobExecutor; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobFileAppender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.*; + + +/** + * handler thread + * @author xuxueli 2016-1-16 19:52:47 + */ +public class JobThread extends Thread{ + private static Logger logger = LoggerFactory.getLogger(JobThread.class); + + private int jobId; + private IJobHandler handler; + private LinkedBlockingQueue<TriggerParam> triggerQueue; + private Set<Long> triggerLogIdSet; // avoid repeat trigger for the same TRIGGER_LOG_ID + + private volatile boolean toStop = false; + private String stopReason; + + private boolean running = false; // if running job + private int idleTimes = 0; // idel times + + + public JobThread(int jobId, IJobHandler handler) { + this.jobId = jobId; + this.handler = handler; + this.triggerQueue = new LinkedBlockingQueue<TriggerParam>(); + this.triggerLogIdSet = Collections.synchronizedSet(new HashSet<Long>()); + + // assign job thread name + this.setName("xxl-job, JobThread-"+jobId+"-"+System.currentTimeMillis()); + } + public IJobHandler getHandler() { + return handler; + } + + /** + * new trigger to queue + * + * @param triggerParam + * @return + */ + public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) { + // avoid repeat + if (triggerLogIdSet.contains(triggerParam.getLogId())) { + logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId()); + return new ReturnT<String>(ReturnT.FAIL_CODE, "repeate trigger job, logId:" + triggerParam.getLogId()); + } + + triggerLogIdSet.add(triggerParam.getLogId()); + triggerQueue.add(triggerParam); + return ReturnT.SUCCESS; + } + + /** + * kill job thread + * + * @param stopReason + */ + public void toStop(String stopReason) { + /** + * Thread.interrupt只支持终止线程的阻塞状态(wait、join、sleep), + * 在阻塞出抛出InterruptedException异常,但是并不会终止运行的线程本身; + * 所以需要注意,此处彻底销毁本线程,需要通过共享变量方式; + */ + this.toStop = true; + this.stopReason = stopReason; + } + + /** + * is running job + * @return + */ + public boolean isRunningOrHasQueue() { + return running || triggerQueue.size()>0; + } + + @Override + public void run() { + + // init + try { + handler.init(); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + + // execute + while(!toStop){ + running = false; + idleTimes++; + + TriggerParam triggerParam = null; + try { + // to check toStop signal, we need cycle, so wo cannot use queue.take(), instand of poll(timeout) + triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS); + if (triggerParam!=null) { + running = true; + idleTimes = 0; + triggerLogIdSet.remove(triggerParam.getLogId()); + + // log filename, like "logPath/yyyy-MM-dd/9999.log" + String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId()); + XxlJobContext xxlJobContext = new XxlJobContext( + triggerParam.getJobId(), + triggerParam.getExecutorParams(), + logFileName, + triggerParam.getBroadcastIndex(), + triggerParam.getBroadcastTotal()); + + // init job context + XxlJobContext.setXxlJobContext(xxlJobContext); + + // execute + XxlJobHelper.log("<br>----------- xxl-job job execute start -----------<br>----------- Param:" + xxlJobContext.getJobParam()); + + if (triggerParam.getExecutorTimeout() > 0) { + // limit timeout + Thread futureThread = null; + try { + FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + + // init job context + XxlJobContext.setXxlJobContext(xxlJobContext); + + handler.execute(); + return true; + } + }); + futureThread = new Thread(futureTask); + futureThread.start(); + + Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS); + } catch (TimeoutException e) { + + XxlJobHelper.log("<br>----------- xxl-job job execute timeout"); + XxlJobHelper.log(e); + + // handle result + XxlJobHelper.handleTimeout("job execute timeout "); + } finally { + futureThread.interrupt(); + } + } else { + // just execute + handler.execute(); + } + + // valid execute handle data + if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) { + XxlJobHelper.handleFail("job handle result lost."); + } else { + String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg(); + tempHandleMsg = (tempHandleMsg!=null&&tempHandleMsg.length()>50000) + ?tempHandleMsg.substring(0, 50000).concat("...") + :tempHandleMsg; + XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg); + } + XxlJobHelper.log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- Result: handleCode=" + + XxlJobContext.getXxlJobContext().getHandleCode() + + ", handleMsg = " + + XxlJobContext.getXxlJobContext().getHandleMsg() + ); + + } else { + if (idleTimes > 30) { + if(triggerQueue.size() == 0) { // avoid concurrent trigger causes jobId-lost + XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit."); + } + } + } + } catch (Throwable e) { + if (toStop) { + XxlJobHelper.log("<br>----------- JobThread toStop, stopReason:" + stopReason); + } + + // handle result + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + String errorMsg = stringWriter.toString(); + + XxlJobHelper.handleFail(errorMsg); + + XxlJobHelper.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- xxl-job job execute end(error) -----------"); + } finally { + if(triggerParam != null) { + // callback handler info + if (!toStop) { + // commonm + TriggerCallbackThread.pushCallBack(new HandleCallbackParam( + triggerParam.getLogId(), + triggerParam.getLogDateTime(), + XxlJobContext.getXxlJobContext().getHandleCode(), + XxlJobContext.getXxlJobContext().getHandleMsg() ) + ); + } else { + // is killed + TriggerCallbackThread.pushCallBack(new HandleCallbackParam( + triggerParam.getLogId(), + triggerParam.getLogDateTime(), + XxlJobContext.HANDLE_CODE_FAIL, + stopReason + " [job running, killed]" ) + ); + } + } + } + } + + // callback trigger request in queue + while(triggerQueue !=null && triggerQueue.size()>0){ + TriggerParam triggerParam = triggerQueue.poll(); + if (triggerParam!=null) { + // is killed + TriggerCallbackThread.pushCallBack(new HandleCallbackParam( + triggerParam.getLogId(), + triggerParam.getLogDateTime(), + XxlJobContext.HANDLE_CODE_FAIL, + stopReason + " [job not executed, in the job queue, killed.]") + ); + } + } + + // destroy + try { + handler.destroy(); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + + logger.info(">>>>>>>>>>> xxl-job JobThread stoped, hashCode:{}", Thread.currentThread()); + } +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java new file mode 100644 index 0000000..40acac0 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java @@ -0,0 +1,260 @@ +package com.xxl.job.core.thread; + +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobContext; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.enums.RegistryConfig; +import com.xxl.job.core.executor.XxlJobExecutor; +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.util.FileUtil; +import com.xxl.job.core.util.JdkSerializeTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Created by xuxueli on 16/7/22. + */ +public class TriggerCallbackThread { + private static Logger logger = LoggerFactory.getLogger(TriggerCallbackThread.class); + + private static TriggerCallbackThread instance = new TriggerCallbackThread(); + public static TriggerCallbackThread getInstance(){ + return instance; + } + + /** + * job results callback queue + */ + private LinkedBlockingQueue<HandleCallbackParam> callBackQueue = new LinkedBlockingQueue<HandleCallbackParam>(); + public static void pushCallBack(HandleCallbackParam callback){ + getInstance().callBackQueue.add(callback); + logger.debug(">>>>>>>>>>> xxl-job, push callback request, logId:{}", callback.getLogId()); + } + + /** + * callback thread + */ + private Thread triggerCallbackThread; + private Thread triggerRetryCallbackThread; + private volatile boolean toStop = false; + public void start() { + + // valid + if (XxlJobExecutor.getAdminBizList() == null) { + logger.warn(">>>>>>>>>>> xxl-job, executor callback config fail, adminAddresses is null."); + return; + } + + // callback + triggerCallbackThread = new Thread(new Runnable() { + + @Override + public void run() { + + // normal callback + while(!toStop){ + try { + HandleCallbackParam callback = getInstance().callBackQueue.take(); + if (callback != null) { + + // callback list param + List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>(); + int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); + callbackParamList.add(callback); + + // callback, will retry if error + if (callbackParamList!=null && callbackParamList.size()>0) { + doCallback(callbackParamList); + } + } + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + } + + // last callback + try { + List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>(); + int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); + if (callbackParamList!=null && callbackParamList.size()>0) { + doCallback(callbackParamList); + } + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + logger.info(">>>>>>>>>>> xxl-job, executor callback thread destroy."); + + } + }); + triggerCallbackThread.setDaemon(true); + triggerCallbackThread.setName("xxl-job, executor TriggerCallbackThread"); + triggerCallbackThread.start(); + + + // retry + triggerRetryCallbackThread = new Thread(new Runnable() { + @Override + public void run() { + while(!toStop){ + try { + retryFailCallbackFile(); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + + } + try { + TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, executor retry callback thread destroy."); + } + }); + triggerRetryCallbackThread.setDaemon(true); + triggerRetryCallbackThread.start(); + + } + public void toStop(){ + toStop = true; + // stop callback, interrupt and wait + if (triggerCallbackThread != null) { // support empty admin address + triggerCallbackThread.interrupt(); + try { + triggerCallbackThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // stop retry, interrupt and wait + if (triggerRetryCallbackThread != null) { + triggerRetryCallbackThread.interrupt(); + try { + triggerRetryCallbackThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + } + + /** + * do callback, will retry if error + * @param callbackParamList + */ + private void doCallback(List<HandleCallbackParam> callbackParamList){ + boolean callbackRet = false; + // callback, will retry if error + for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { + try { + ReturnT<String> callbackResult = adminBiz.callback(callbackParamList); + if (callbackResult!=null && ReturnT.SUCCESS_CODE == callbackResult.getCode()) { + callbackLog(callbackParamList, "<br>----------- xxl-job job callback finish."); + callbackRet = true; + break; + } else { + callbackLog(callbackParamList, "<br>----------- xxl-job job callback fail, callbackResult:" + callbackResult); + } + } catch (Exception e) { + callbackLog(callbackParamList, "<br>----------- xxl-job job callback error, errorMsg:" + e.getMessage()); + } + } + if (!callbackRet) { + appendFailCallbackFile(callbackParamList); + } + } + + /** + * callback log + */ + private void callbackLog(List<HandleCallbackParam> callbackParamList, String logContent){ + for (HandleCallbackParam callbackParam: callbackParamList) { + String logFileName = XxlJobFileAppender.makeLogFileName(new Date(callbackParam.getLogDateTim()), callbackParam.getLogId()); + XxlJobContext.setXxlJobContext(new XxlJobContext( + -1, + null, + logFileName, + -1, + -1)); + XxlJobHelper.log(logContent); + } + } + + + // ---------------------- fail-callback file ---------------------- + + private static String failCallbackFilePath = XxlJobFileAppender.getLogPath().concat(File.separator).concat("callbacklog").concat(File.separator); + private static String failCallbackFileName = failCallbackFilePath.concat("xxl-job-callback-{x}").concat(".log"); + + private void appendFailCallbackFile(List<HandleCallbackParam> callbackParamList){ + // valid + if (callbackParamList==null || callbackParamList.size()==0) { + return; + } + + // append file + byte[] callbackParamList_bytes = JdkSerializeTool.serialize(callbackParamList); + + File callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis()))); + if (callbackLogFile.exists()) { + for (int i = 0; i < 100; i++) { + callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis()).concat("-").concat(String.valueOf(i)) )); + if (!callbackLogFile.exists()) { + break; + } + } + } + FileUtil.writeFileContent(callbackLogFile, callbackParamList_bytes); + } + + private void retryFailCallbackFile(){ + + // valid + File callbackLogPath = new File(failCallbackFilePath); + if (!callbackLogPath.exists()) { + return; + } + if (callbackLogPath.isFile()) { + callbackLogPath.delete(); + } + if (!(callbackLogPath.isDirectory() && callbackLogPath.list()!=null && callbackLogPath.list().length>0)) { + return; + } + + // load and clear file, retry + for (File callbaclLogFile: callbackLogPath.listFiles()) { + byte[] callbackParamList_bytes = FileUtil.readFileContent(callbaclLogFile); + + // avoid empty file + if(callbackParamList_bytes == null || callbackParamList_bytes.length < 1){ + callbaclLogFile.delete(); + continue; + } + + List<HandleCallbackParam> callbackParamList = (List<HandleCallbackParam>) JdkSerializeTool.deserialize(callbackParamList_bytes, List.class); + + callbaclLogFile.delete(); + doCallback(callbackParamList); + } + + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java new file mode 100644 index 0000000..71afe0a --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java @@ -0,0 +1,156 @@ +package com.xxl.job.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * date util + * + * @author xuxueli 2018-08-19 01:24:11 + */ +public class DateUtil { + + // ---------------------- format parse ---------------------- + private static Logger logger = LoggerFactory.getLogger(DateUtil.class); + + private static final String DATE_FORMAT = "yyyy-MM-dd"; + private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private static final ThreadLocal<Map<String, DateFormat>> dateFormatThreadLocal = new ThreadLocal<Map<String, DateFormat>>(); + private static DateFormat getDateFormat(String pattern) { + if (pattern==null || pattern.trim().length()==0) { + throw new IllegalArgumentException("pattern cannot be empty."); + } + + Map<String, DateFormat> dateFormatMap = dateFormatThreadLocal.get(); + if(dateFormatMap!=null && dateFormatMap.containsKey(pattern)){ + return dateFormatMap.get(pattern); + } + + synchronized (dateFormatThreadLocal) { + if (dateFormatMap == null) { + dateFormatMap = new HashMap<String, DateFormat>(); + } + dateFormatMap.put(pattern, new SimpleDateFormat(pattern)); + dateFormatThreadLocal.set(dateFormatMap); + } + + return dateFormatMap.get(pattern); + } + + /** + * format datetime. like "yyyy-MM-dd" + * + * @param date + * @return + * @throws ParseException + */ + public static String formatDate(Date date) { + return format(date, DATE_FORMAT); + } + + /** + * format date. like "yyyy-MM-dd HH:mm:ss" + * + * @param date + * @return + * @throws ParseException + */ + public static String formatDateTime(Date date) { + return format(date, DATETIME_FORMAT); + } + + /** + * format date + * + * @param date + * @param patten + * @return + * @throws ParseException + */ + public static String format(Date date, String patten) { + return getDateFormat(patten).format(date); + } + + /** + * parse date string, like "yyyy-MM-dd HH:mm:s" + * + * @param dateString + * @return + * @throws ParseException + */ + public static Date parseDate(String dateString){ + return parse(dateString, DATE_FORMAT); + } + + /** + * parse datetime string, like "yyyy-MM-dd HH:mm:ss" + * + * @param dateString + * @return + * @throws ParseException + */ + public static Date parseDateTime(String dateString) { + return parse(dateString, DATETIME_FORMAT); + } + + /** + * parse date + * + * @param dateString + * @param pattern + * @return + * @throws ParseException + */ + public static Date parse(String dateString, String pattern) { + try { + Date date = getDateFormat(pattern).parse(dateString); + return date; + } catch (Exception e) { + logger.warn("parse date error, dateString = {}, pattern={}; errorMsg = {}", dateString, pattern, e.getMessage()); + return null; + } + } + + + // ---------------------- add date ---------------------- + + public static Date addYears(final Date date, final int amount) { + return add(date, Calendar.YEAR, amount); + } + + public static Date addMonths(final Date date, final int amount) { + return add(date, Calendar.MONTH, amount); + } + + public static Date addDays(final Date date, final int amount) { + return add(date, Calendar.DAY_OF_MONTH, amount); + } + + public static Date addHours(final Date date, final int amount) { + return add(date, Calendar.HOUR_OF_DAY, amount); + } + + public static Date addMinutes(final Date date, final int amount) { + return add(date, Calendar.MINUTE, amount); + } + + private static Date add(final Date date, final int calendarField, final int amount) { + if (date == null) { + return null; + } + final Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(calendarField, amount); + return c.getTime(); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java new file mode 100644 index 0000000..d44ef4b --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java @@ -0,0 +1,181 @@ +package com.xxl.job.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * file tool + * + * @author xuxueli 2017-12-29 17:56:48 + */ +public class FileUtil { + private static Logger logger = LoggerFactory.getLogger(FileUtil.class); + + + /** + * delete recursively + * + * @param root + * @return + */ + public static boolean deleteRecursively(File root) { + if (root != null && root.exists()) { + if (root.isDirectory()) { + File[] children = root.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + return root.delete(); + } + return false; + } + + + public static void deleteFile(String fileName) { + // file + File file = new File(fileName); + if (file.exists()) { + file.delete(); + } + } + + + public static void writeFileContent(File file, byte[] data) { + + // file + if (!file.exists()) { + file.getParentFile().mkdirs(); + } + + // append file content + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + fos.write(data); + fos.flush(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + + } + + public static byte[] readFileContent(File file) { + Long filelength = file.length(); + byte[] filecontent = new byte[filelength.intValue()]; + + FileInputStream in = null; + try { + in = new FileInputStream(file); + in.read(filecontent); + in.close(); + + return filecontent; + } catch (Exception e) { + logger.error(e.getMessage(), e); + return null; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + } + + + /*public static void appendFileLine(String fileName, String content) { + + // file + File file = new File(fileName); + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + return; + } + } + + // content + if (content == null) { + content = ""; + } + content += "\r\n"; + + // append file content + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, true); + fos.write(content.getBytes("utf-8")); + fos.flush(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + + } + + public static List<String> loadFileLines(String fileName){ + + List<String> result = new ArrayList<>(); + + // valid log file + File file = new File(fileName); + if (!file.exists()) { + return result; + } + + // read file + StringBuffer logContentBuffer = new StringBuffer(); + int toLineNum = 0; + LineNumberReader reader = null; + try { + //reader = new LineNumberReader(new FileReader(logFile)); + reader = new LineNumberReader(new InputStreamReader(new FileInputStream(file), "utf-8")); + String line = null; + while ((line = reader.readLine())!=null) { + if (line!=null && line.trim().length()>0) { + result.add(line); + } + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + + return result; + }*/ + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java new file mode 100644 index 0000000..85682a6 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java @@ -0,0 +1,88 @@ +package com.xxl.job.core.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +/** + * @author xuxueli 2020-04-11 20:56:31 + */ +public class GsonTool { + + private static Gson gson = null; + static { + gson= new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + } + + /** + * Object 转成 json + * + * @param src + * @return String + */ + public static String toJson(Object src) { + return gson.toJson(src); + } + + /** + * json 转成 特定的cls的Object + * + * @param json + * @param classOfT + * @return + */ + public static <T> T fromJson(String json, Class<T> classOfT) { + return gson.fromJson(json, classOfT); + } + + /** + * json 转成 特定的 rawClass<classOfT> 的Object + * + * @param json + * @param classOfT + * @param argClassOfT + * @return + */ + public static <T> T fromJson(String json, Class<T> classOfT, Class argClassOfT) { + Type type = new ParameterizedType4ReturnT(classOfT, new Class[]{argClassOfT}); + return gson.fromJson(json, type); + } + public static class ParameterizedType4ReturnT implements ParameterizedType { + private final Class raw; + private final Type[] args; + public ParameterizedType4ReturnT(Class raw, Type[] args) { + this.raw = raw; + this.args = args != null ? args : new Type[0]; + } + @Override + public Type[] getActualTypeArguments() { + return args; + } + @Override + public Type getRawType() { + return raw; + } + @Override + public Type getOwnerType() {return null;} + } + + /** + * json 转成 特定的cls的list + * + * @param json + * @param classOfT + * @return + */ + public static <T> List<T> fromJsonList(String json, Class<T> classOfT) { + return gson.fromJson( + json, + new TypeToken<List<T>>() { + }.getType() + ); + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java new file mode 100644 index 0000000..c97c4fe --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java @@ -0,0 +1,203 @@ +package com.xxl.job.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.regex.Pattern; + +/** + * ip tool + * + * @author xuxueli 2016-5-22 11:38:05 + */ +public class IpUtil { + private static final Logger logger = LoggerFactory.getLogger(IpUtil.class); + + private static final String ANYHOST_VALUE = "0.0.0.0"; + private static final String LOCALHOST_VALUE = "127.0.0.1"; + private static final Pattern IP_PATTERN = Pattern.compile("\\d{1,3}(\\.\\d{1,3}){3,5}$"); + + + + private static volatile InetAddress LOCAL_ADDRESS = null; + + // ---------------------- valid ---------------------- + + private static InetAddress toValidAddress(InetAddress address) { + if (address instanceof Inet6Address) { + Inet6Address v6Address = (Inet6Address) address; + if (isPreferIPV6Address()) { + return normalizeV6Address(v6Address); + } + } + if (isValidV4Address(address)) { + return address; + } + return null; + } + + private static boolean isPreferIPV6Address() { + return Boolean.getBoolean("java.net.preferIPv6Addresses"); + } + + /** + * valid Inet4Address + * + * @param address + * @return + */ + private static boolean isValidV4Address(InetAddress address) { + if (address == null || address.isLoopbackAddress()) { + return false; + } + String name = address.getHostAddress(); + boolean result = (name != null + && IP_PATTERN.matcher(name).matches() + && !ANYHOST_VALUE.equals(name) + && !LOCALHOST_VALUE.equals(name)); + return result; + } + + + /** + * normalize the ipv6 Address, convert scope name to scope id. + * e.g. + * convert + * fe80:0:0:0:894:aeec:f37d:23e1%en0 + * to + * fe80:0:0:0:894:aeec:f37d:23e1%5 + * <p> + * The %5 after ipv6 address is called scope id. + * see java doc of {@link Inet6Address} for more details. + * + * @param address the input address + * @return the normalized address, with scope id converted to int + */ + private static InetAddress normalizeV6Address(Inet6Address address) { + String addr = address.getHostAddress(); + int i = addr.lastIndexOf('%'); + if (i > 0) { + try { + return InetAddress.getByName(addr.substring(0, i) + '%' + address.getScopeId()); + } catch (UnknownHostException e) { + // ignore + logger.debug("Unknown IPV6 address: ", e); + } + } + return address; + } + + // ---------------------- find ip ---------------------- + + + private static InetAddress getLocalAddress0() { + InetAddress localAddress = null; + try { + localAddress = InetAddress.getLocalHost(); + InetAddress addressItem = toValidAddress(localAddress); + if (addressItem != null) { + return addressItem; + } + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + + try { + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + if (null == interfaces) { + return localAddress; + } + while (interfaces.hasMoreElements()) { + try { + NetworkInterface network = interfaces.nextElement(); + if (network.isLoopback() || network.isVirtual() || !network.isUp()) { + continue; + } + Enumeration<InetAddress> addresses = network.getInetAddresses(); + while (addresses.hasMoreElements()) { + try { + InetAddress addressItem = toValidAddress(addresses.nextElement()); + if (addressItem != null) { + try { + if(addressItem.isReachable(100)){ + return addressItem; + } + } catch (IOException e) { + // ignore + } + } + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + return localAddress; + } + + + // ---------------------- tool ---------------------- + + /** + * Find first valid IP from local network card + * + * @return first valid local IP + */ + public static InetAddress getLocalAddress() { + if (LOCAL_ADDRESS != null) { + return LOCAL_ADDRESS; + } + InetAddress localAddress = getLocalAddress0(); + LOCAL_ADDRESS = localAddress; + return localAddress; + } + + /** + * get ip address + * + * @return String + */ + public static String getIp(){ + return getLocalAddress().getHostAddress(); + } + + /** + * get ip:port + * + * @param port + * @return String + */ + public static String getIpPort(int port){ + String ip = getIp(); + return getIpPort(ip, port); + } + + public static String getIpPort(String ip, int port){ + if (ip==null) { + return null; + } + return ip.concat(":").concat(String.valueOf(port)); + } + + public static Object[] parseIpPort(String address){ + String[] array = address.split(":"); + + String host = array[0]; + int port = Integer.parseInt(array[1]); + + return new Object[]{host, port}; + } + + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java new file mode 100644 index 0000000..434585f --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java @@ -0,0 +1,73 @@ +package com.xxl.job.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; + +/** + * @author xuxueli 2020-04-12 0:14:00 + */ +public class JdkSerializeTool { + private static Logger logger = LoggerFactory.getLogger(JdkSerializeTool.class); + + + // ------------------------ serialize and unserialize ------------------------ + + /** + * 将对象-->byte[] (由于jedis中不支持直接存储object所以转换成byte[]存入) + * + * @param object + * @return + */ + public static byte[] serialize(Object object) { + ObjectOutputStream oos = null; + ByteArrayOutputStream baos = null; + try { + // 序列化 + baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(object); + byte[] bytes = baos.toByteArray(); + return bytes; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + try { + oos.close(); + baos.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + + /** + * 将byte[] -->Object + * + * @param bytes + * @return + */ + public static <T> Object deserialize(byte[] bytes, Class<T> clazz) { + ByteArrayInputStream bais = null; + try { + // 反序列化 + bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + return ois.readObject(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + try { + bais.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java new file mode 100644 index 0000000..41d285f --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java @@ -0,0 +1,70 @@ +package com.xxl.job.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.ServerSocket; + +/** + * net util + * + * @author xuxueli 2017-11-29 17:00:25 + */ +public class NetUtil { + private static Logger logger = LoggerFactory.getLogger(NetUtil.class); + + /** + * find avaliable port + * + * @param defaultPort + * @return + */ + public static int findAvailablePort(int defaultPort) { + int portTmp = defaultPort; + while (portTmp < 65535) { + if (!isPortUsed(portTmp)) { + return portTmp; + } else { + portTmp++; + } + } + portTmp = defaultPort--; + while (portTmp > 0) { + if (!isPortUsed(portTmp)) { + return portTmp; + } else { + portTmp--; + } + } + throw new RuntimeException("no available port."); + } + + /** + * check port used + * + * @param port + * @return + */ + public static boolean isPortUsed(int port) { + boolean used = false; + ServerSocket serverSocket = null; + try { + serverSocket = new ServerSocket(port); + used = false; + } catch (IOException e) { + logger.info(">>>>>>>>>>> xxl-job, port[{}] is in use.", port); + used = true; + } finally { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + logger.info(""); + } + } + } + return used; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java new file mode 100644 index 0000000..a27e27b --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java @@ -0,0 +1,228 @@ +package com.xxl.job.core.util; + +import com.xxl.job.core.context.XxlJobHelper; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 1、内嵌编译器如"PythonInterpreter"无法引用扩展包,因此推荐使用java调用控制台进程方式"Runtime.getRuntime().exec()"来运行脚本(shell或python); + * 2、因为通过java调用控制台进程方式实现,需要保证目标机器PATH路径正确配置对应编译器; + * 3、暂时脚本执行日志只能在脚本执行结束后一次性获取,无法保证实时性;因此为确保日志实时性,可改为将脚本打印的日志存储在指定的日志文件上; + * 4、python 异常输出优先级高于标准输出,体现在Log文件中,因此推荐通过logging方式打日志保持和异常信息一致;否则用prinf日志顺序会错乱 + * + * Created by xuxueli on 17/2/25. + */ +public class ScriptUtil { + + /** + * make script file + * + * @param scriptFileName + * @param content + * @throws IOException + */ + public static void markScriptFile(String scriptFileName, String content) throws IOException { + // make file, filePath/gluesource/666-123456789.py + FileOutputStream fileOutputStream = null; + try { + fileOutputStream = new FileOutputStream(scriptFileName); + fileOutputStream.write(content.getBytes("UTF-8")); + fileOutputStream.close(); + } catch (Exception e) { + throw e; + }finally{ + if(fileOutputStream != null){ + fileOutputStream.close(); + } + } + } + + /** + * 脚本执行,日志文件实时输出 + * + * @param command + * @param scriptFile + * @param logFile + * @param params + * @return + * @throws IOException + */ + public static int execToFile(String command, String scriptFile, String logFile, String... params) throws IOException { + + FileOutputStream fileOutputStream = null; + Thread inputThread = null; + Thread errThread = null; + try { + // file + fileOutputStream = new FileOutputStream(logFile, true); + + // command + List<String> cmdarray = new ArrayList<>(); + cmdarray.add(command); + cmdarray.add(scriptFile); + if (params!=null && params.length>0) { + for (String param:params) { + cmdarray.add(param); + } + } + String[] cmdarrayFinal = cmdarray.toArray(new String[cmdarray.size()]); + + // process-exec + final Process process = Runtime.getRuntime().exec(cmdarrayFinal); + + // log-thread + final FileOutputStream finalFileOutputStream = fileOutputStream; + inputThread = new Thread(new Runnable() { + @Override + public void run() { + try { + copy(process.getInputStream(), finalFileOutputStream, new byte[1024]); + } catch (IOException e) { + XxlJobHelper.log(e); + } + } + }); + errThread = new Thread(new Runnable() { + @Override + public void run() { + try { + copy(process.getErrorStream(), finalFileOutputStream, new byte[1024]); + } catch (IOException e) { + XxlJobHelper.log(e); + } + } + }); + inputThread.start(); + errThread.start(); + + // process-wait + int exitValue = process.waitFor(); // exit code: 0=success, 1=error + + // log-thread join + inputThread.join(); + errThread.join(); + + return exitValue; + } catch (Exception e) { + XxlJobHelper.log(e); + return -1; + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException e) { + XxlJobHelper.log(e); + } + + } + if (inputThread != null && inputThread.isAlive()) { + inputThread.interrupt(); + } + if (errThread != null && errThread.isAlive()) { + errThread.interrupt(); + } + } + } + + /** + * 数据流Copy(Input自动关闭,Output不处理) + * + * @param inputStream + * @param outputStream + * @param buffer + * @return + * @throws IOException + */ + private static long copy(InputStream inputStream, OutputStream outputStream, byte[] buffer) throws IOException { + try { + long total = 0; + for (;;) { + int res = inputStream.read(buffer); + if (res == -1) { + break; + } + if (res > 0) { + total += res; + if (outputStream != null) { + outputStream.write(buffer, 0, res); + } + } + } + outputStream.flush(); + //out = null; + inputStream.close(); + inputStream = null; + return total; + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + /** + * 脚本执行,日志文件实时输出 + * + * 优点:支持将目标数据实时输出到指定日志文件中去 + * 缺点: + * 标准输出和错误输出优先级固定,可能和脚本中顺序不一致 + * Java无法实时获取 + * + * <!-- commons-exec --> + * <dependency> + * <groupId>org.apache.commons</groupId> + * <artifactId>commons-exec</artifactId> + * <version>${commons-exec.version}</version> + * </dependency> + * + * @param command + * @param scriptFile + * @param logFile + * @param params + * @return + * @throws IOException + */ + /*public static int execToFileB(String command, String scriptFile, String logFile, String... params) throws IOException { + // 标准输出:print (null if watchdog timeout) + // 错误输出:logging + 异常 (still exists if watchdog timeout) + // 标准输入 + + FileOutputStream fileOutputStream = null; // + try { + fileOutputStream = new FileOutputStream(logFile, true); + PumpStreamHandler streamHandler = new PumpStreamHandler(fileOutputStream, fileOutputStream, null); + + // command + CommandLine commandline = new CommandLine(command); + commandline.addArgument(scriptFile); + if (params!=null && params.length>0) { + commandline.addArguments(params); + } + + // exec + DefaultExecutor exec = new DefaultExecutor(); + exec.setExitValues(null); + exec.setStreamHandler(streamHandler); + int exitValue = exec.execute(commandline); // exit code: 0=success, 1=error + return exitValue; + } catch (Exception e) { + XxlJobLogger.log(e); + return -1; + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException e) { + XxlJobLogger.log(e); + } + + } + } + }*/ + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java new file mode 100644 index 0000000..7671668 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java @@ -0,0 +1,46 @@ +//package com.xxl.job.core.util; +// +///** +// * sharding vo +// * @author xuxueli 2017-07-25 21:26:38 +// */ +//public class ShardingUtil { +// +// private static InheritableThreadLocal<ShardingVO> contextHolder = new InheritableThreadLocal<ShardingVO>(); +// +// public static class ShardingVO { +// +// private int index; // sharding index +// private int total; // sharding total +// +// public ShardingVO(int index, int total) { +// this.index = index; +// this.total = total; +// } +// +// public int getIndex() { +// return index; +// } +// +// public void setIndex(int index) { +// this.index = index; +// } +// +// public int getTotal() { +// return total; +// } +// +// public void setTotal(int total) { +// this.total = total; +// } +// } +// +// public static void setShardingVo(ShardingVO shardingVo){ +// contextHolder.set(shardingVo); +// } +// +// public static ShardingVO getShardingVo(){ +// return contextHolder.get(); +// } +// +//} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java new file mode 100644 index 0000000..63c39c4 --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java @@ -0,0 +1,24 @@ +package com.xxl.job.core.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * @author xuxueli 2018-10-20 20:07:26 + */ +public class ThrowableUtil { + + /** + * parse error to string + * + * @param e + * @return + */ + public static String toString(Throwable e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + String errorMsg = stringWriter.toString(); + return errorMsg; + } + +} diff --git a/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java new file mode 100644 index 0000000..d22a73e --- /dev/null +++ b/xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java @@ -0,0 +1,159 @@ +package com.xxl.job.core.util; + +import com.xxl.job.core.biz.model.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.*; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Map; + +/** + * @author xuxueli 2018-11-25 00:55:31 + */ +public class XxlJobRemotingUtil { + private static Logger logger = LoggerFactory.getLogger(XxlJobRemotingUtil.class); + public static final String XXL_JOB_ACCESS_TOKEN = "XXL-JOB-ACCESS-TOKEN"; + + + // trust-https start + private static void trustAllHosts(HttpsURLConnection connection) { + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + SSLSocketFactory newFactory = sc.getSocketFactory(); + + connection.setSSLSocketFactory(newFactory); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + connection.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } + private static final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + }}; + // trust-https end + + + /** + * post + * + * @param url + * @param accessToken + * @param timeout + * @param requestObj + * @param returnTargClassOfT + * @return + */ + public static ReturnT postBody(String url, String accessToken, int timeout, Object requestObj, Class returnTargClassOfT) { + HttpURLConnection connection = null; + BufferedReader bufferedReader = null; + try { + // connection + URL realUrl = new URL(url); + connection = (HttpURLConnection) realUrl.openConnection(); + + // trust-https + boolean useHttps = url.startsWith("https"); + if (useHttps) { + HttpsURLConnection https = (HttpsURLConnection) connection; + trustAllHosts(https); + } + + // connection setting + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setReadTimeout(timeout * 1000); + connection.setConnectTimeout(3 * 1000); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); + + if(accessToken!=null && accessToken.trim().length()>0){ + connection.setRequestProperty(XXL_JOB_ACCESS_TOKEN, accessToken); + } + + // do connection + connection.connect(); + + // write requestBody + if (requestObj != null) { + String requestBody = GsonTool.toJson(requestObj); + + DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); + dataOutputStream.write(requestBody.getBytes("UTF-8")); + dataOutputStream.flush(); + dataOutputStream.close(); + } + + /*byte[] requestBodyBytes = requestBody.getBytes("UTF-8"); + connection.setRequestProperty("Content-Length", String.valueOf(requestBodyBytes.length)); + OutputStream outwritestream = connection.getOutputStream(); + outwritestream.write(requestBodyBytes); + outwritestream.flush(); + outwritestream.close();*/ + + // valid StatusCode + int statusCode = connection.getResponseCode(); + if (statusCode != 200) { + return new ReturnT<String>(ReturnT.FAIL_CODE, "xxl-job remoting fail, StatusCode("+ statusCode +") invalid. for url : " + url); + } + + // result + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + String resultJson = result.toString(); + + // parse returnT + try { + ReturnT returnT = GsonTool.fromJson(resultJson, ReturnT.class, returnTargClassOfT); + return returnT; + } catch (Exception e) { + logger.error("xxl-job remoting (url="+url+") response content invalid("+ resultJson +").", e); + return new ReturnT<String>(ReturnT.FAIL_CODE, "xxl-job remoting (url="+url+") response content invalid("+ resultJson +")."); + } + + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT<String>(ReturnT.FAIL_CODE, "xxl-job remoting error("+ e.getMessage() +"), for url : " + url); + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + if (connection != null) { + connection.disconnect(); + } + } catch (Exception e2) { + logger.error(e2.getMessage(), e2); + } + } + } + +} diff --git a/xxl-job/xxl-job-executor-samples/pom.xml b/xxl-job/xxl-job-executor-samples/pom.xml new file mode 100644 index 0000000..3ad85ff --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/pom.xml @@ -0,0 +1,18 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job</artifactId> + <version>2.3.1</version> + </parent> + <artifactId>xxl-job-executor-samples</artifactId> + <packaging>pom</packaging> + + <modules> + <module>xxl-job-executor-sample-frameless</module> + <module>xxl-job-executor-sample-springboot</module> + </modules> + +</project> \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml new file mode 100644 index 0000000..e01d4d4 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-executor-samples</artifactId> + <version>2.3.1</version> + </parent> + <artifactId>xxl-job-executor-sample-frameless</artifactId> + <packaging>jar</packaging> + + <name>${project.artifactId}</name> + <description>Example executor project for spring boot.</description> + <url>https://www.xuxueli.com/</url> + + + <dependencies> + + <!-- slf4j --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>${slf4j-api.version}</version> + </dependency> + <!-- junit --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit-jupiter.version}</version> + <scope>test</scope> + </dependency> + + <!-- xxl-job-core --> + <dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>${project.parent.version}</version> + </dependency> + + </dependencies> + + +</project> \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/FramelessApplication.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/FramelessApplication.java new file mode 100644 index 0000000..1e7cb7d --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/FramelessApplication.java @@ -0,0 +1,38 @@ +package com.xxl.job.executor.sample.frameless; + +import com.xxl.job.executor.sample.frameless.config.FrameLessXxlJobConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * @author xuxueli 2018-10-31 19:05:43 + */ +public class FramelessApplication { + private static Logger logger = LoggerFactory.getLogger(FramelessApplication.class); + + public static void main(String[] args) { + + try { + // start + FrameLessXxlJobConfig.getInstance().initXxlJobExecutor(); + + // Blocks until interrupted + while (true) { + try { + TimeUnit.HOURS.sleep(1); + } catch (InterruptedException e) { + break; + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + // destroy + FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor(); + } + + } + +} diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java new file mode 100644 index 0000000..1d882dc --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java @@ -0,0 +1,93 @@ +package com.xxl.job.executor.sample.frameless.config; + +import com.xxl.job.executor.sample.frameless.jobhandler.SampleXxlJob; +import com.xxl.job.core.executor.impl.XxlJobSimpleExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Properties; + +/** + * @author xuxueli 2018-10-31 19:05:43 + */ +public class FrameLessXxlJobConfig { + private static Logger logger = LoggerFactory.getLogger(FrameLessXxlJobConfig.class); + + + private static FrameLessXxlJobConfig instance = new FrameLessXxlJobConfig(); + public static FrameLessXxlJobConfig getInstance() { + return instance; + } + + + private XxlJobSimpleExecutor xxlJobExecutor = null; + + /** + * init + */ + public void initXxlJobExecutor() { + + // load executor prop + Properties xxlJobProp = loadProperties("xxl-job-executor.properties"); + + // init executor + xxlJobExecutor = new XxlJobSimpleExecutor(); + xxlJobExecutor.setAdminAddresses(xxlJobProp.getProperty("xxl.job.admin.addresses")); + xxlJobExecutor.setAccessToken(xxlJobProp.getProperty("xxl.job.accessToken")); + xxlJobExecutor.setAppname(xxlJobProp.getProperty("xxl.job.executor.appname")); + xxlJobExecutor.setAddress(xxlJobProp.getProperty("xxl.job.executor.address")); + xxlJobExecutor.setIp(xxlJobProp.getProperty("xxl.job.executor.ip")); + xxlJobExecutor.setPort(Integer.valueOf(xxlJobProp.getProperty("xxl.job.executor.port"))); + xxlJobExecutor.setLogPath(xxlJobProp.getProperty("xxl.job.executor.logpath")); + xxlJobExecutor.setLogRetentionDays(Integer.valueOf(xxlJobProp.getProperty("xxl.job.executor.logretentiondays"))); + + // registry job bean + xxlJobExecutor.setXxlJobBeanList(Arrays.asList(new SampleXxlJob())); + + // start executor + try { + xxlJobExecutor.start(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + /** + * destroy + */ + public void destroyXxlJobExecutor() { + if (xxlJobExecutor != null) { + xxlJobExecutor.destroy(); + } + } + + + public static Properties loadProperties(String propertyFileName) { + InputStreamReader in = null; + try { + ClassLoader loder = Thread.currentThread().getContextClassLoader(); + + in = new InputStreamReader(loder.getResourceAsStream(propertyFileName), "UTF-8");; + if (in != null) { + Properties prop = new Properties(); + prop.load(in); + return prop; + } + } catch (IOException e) { + logger.error("load {} error!", propertyFileName); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + logger.error("close {} error!", propertyFileName); + } + } + } + return null; + } + +} diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java new file mode 100644 index 0000000..a4eefd1 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java @@ -0,0 +1,251 @@ +package com.xxl.job.executor.sample.frameless.jobhandler; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +/** + * XxlJob开发示例(Bean模式) + * + * 开发步骤: + * 1、任务开发:在Spring Bean实例中,开发Job方法; + * 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 + * 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志; + * 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果; + * + * @author xuxueli 2019-12-11 21:52:51 + */ +public class SampleXxlJob { + private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class); + + + /** + * 1、简单任务示例(Bean模式) + */ + @XxlJob("demoJobHandler") + public void demoJobHandler() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + + for (int i = 0; i < 5; i++) { + XxlJobHelper.log("beat at:" + i); + TimeUnit.SECONDS.sleep(2); + } + // default success + } + + + /** + * 2、分片广播任务 + */ + @XxlJob("shardingJobHandler") + public void shardingJobHandler() throws Exception { + + // 分片参数 + int shardIndex = XxlJobHelper.getShardIndex(); + int shardTotal = XxlJobHelper.getShardTotal(); + + XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal); + + // 业务逻辑 + for (int i = 0; i < shardTotal; i++) { + if (i == shardIndex) { + XxlJobHelper.log("第 {} 片, 命中分片开始处理", i); + } else { + XxlJobHelper.log("第 {} 片, 忽略", i); + } + } + + } + + + /** + * 3、命令行任务 + */ + @XxlJob("commandJobHandler") + public void commandJobHandler() throws Exception { + String command = XxlJobHelper.getJobParam(); + int exitValue = -1; + + BufferedReader bufferedReader = null; + try { + // command process + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(command); + processBuilder.redirectErrorStream(true); + + Process process = processBuilder.start(); + //Process process = Runtime.getRuntime().exec(command); + + BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream()); + bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream)); + + // command log + String line; + while ((line = bufferedReader.readLine()) != null) { + XxlJobHelper.log(line); + } + + // command exit + process.waitFor(); + exitValue = process.exitValue(); + } catch (Exception e) { + XxlJobHelper.log(e); + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + + if (exitValue == 0) { + // default success + } else { + XxlJobHelper.handleFail("command exit value("+exitValue+") is failed"); + } + + } + + + /** + * 4、跨平台Http任务 + * 参数示例: + * "url: http://www.baidu.com\n" + + * "method: get\n" + + * "data: content\n"; + */ + @XxlJob("httpJobHandler") + public void httpJobHandler() throws Exception { + + // param parse + String param = XxlJobHelper.getJobParam(); + if (param==null || param.trim().length()==0) { + XxlJobHelper.log("param["+ param +"] invalid."); + + XxlJobHelper.handleFail(); + return; + } + + String[] httpParams = param.split("\n"); + String url = null; + String method = null; + String data = null; + for (String httpParam: httpParams) { + if (httpParam.startsWith("url:")) { + url = httpParam.substring(httpParam.indexOf("url:") + 4).trim(); + } + if (httpParam.startsWith("method:")) { + method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase(); + } + if (httpParam.startsWith("data:")) { + data = httpParam.substring(httpParam.indexOf("data:") + 5).trim(); + } + } + + // param valid + if (url==null || url.trim().length()==0) { + XxlJobHelper.log("url["+ url +"] invalid."); + + XxlJobHelper.handleFail(); + return; + } + if (method==null || !Arrays.asList("GET", "POST").contains(method)) { + XxlJobHelper.log("method["+ method +"] invalid."); + + XxlJobHelper.handleFail(); + return; + } + boolean isPostMethod = method.equals("POST"); + + // request + HttpURLConnection connection = null; + BufferedReader bufferedReader = null; + try { + // connection + URL realUrl = new URL(url); + connection = (HttpURLConnection) realUrl.openConnection(); + + // connection setting + connection.setRequestMethod(method); + connection.setDoOutput(isPostMethod); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setReadTimeout(5 * 1000); + connection.setConnectTimeout(3 * 1000); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); + + // do connection + connection.connect(); + + // data + if (isPostMethod && data!=null && data.trim().length()>0) { + DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); + dataOutputStream.write(data.getBytes("UTF-8")); + dataOutputStream.flush(); + dataOutputStream.close(); + } + + // valid StatusCode + int statusCode = connection.getResponseCode(); + if (statusCode != 200) { + throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid."); + } + + // result + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + String responseMsg = result.toString(); + + XxlJobHelper.log(responseMsg); + + return; + } catch (Exception e) { + XxlJobHelper.log(e); + + XxlJobHelper.handleFail(); + return; + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + if (connection != null) { + connection.disconnect(); + } + } catch (Exception e2) { + XxlJobHelper.log(e2); + } + } + + } + + /** + * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑; + */ + @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy") + public void demoJobHandler2() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + } + public void init(){ + logger.info("init"); + } + public void destroy(){ + logger.info("destroy"); + } + + +} diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml new file mode 100644 index 0000000..896517e --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE log4j:configuration PUBLIC "-//log4j/log4j Configuration//EN" "log4j.dtd"> +<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" threshold="null" debug="null"> + + <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> + <param name="Target" value="System.out" /> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%-d{yyyy-MM-dd HH:mm:ss} xxl-job-executor-sample-frameless [%c]-[%t]-[%M]-[%L]-[%p] %m%n"/> + </layout> + </appender> + + <appender name="FILE" class="org.apache.log4j.DailyRollingFileAppender"> + <param name="file" value="/data/applogs/xxl-job/xxl-job-executor-sample-frameless.log"/> + <param name="append" value="true"/> + <param name="encoding" value="UTF-8"/> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%-d{yyyy-MM-dd HH:mm:ss} xxl-job-executor-sample-frameless [%c]-[%t]-[%M]-[%L]-[%p] %m%n"/> + </layout> + </appender> + + <root> + <level value="INFO" /> + <appender-ref ref="CONSOLE" /> + <appender-ref ref="FILE" /> + </root> + +</log4j:configuration> \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties new file mode 100644 index 0000000..9b1ab8a --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties @@ -0,0 +1,17 @@ +### xxl-job admin address list, such as "http://address" or "http://address01,http://address02" +xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + +### xxl-job, access token +xxl.job.accessToken=default_token + +### xxl-job executor appname +xxl.job.executor.appname=xxl-job-executor-sample +### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null +xxl.job.executor.address= +### xxl-job executor server-info +xxl.job.executor.ip= +xxl.job.executor.port=9998 +### xxl-job executor log-path +xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler +### xxl-job executor log-retention-days +xxl.job.executor.logretentiondays=30 \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java new file mode 100644 index 0000000..1f9be9a --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java @@ -0,0 +1,12 @@ +package com.xxl.job.executor.sample.frameless.test; + +import org.junit.jupiter.api.Test; + +public class FramelessApplicationTest { + + @Test + public void test(){ + System.out.println("111"); + } + +} diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile new file mode 100644 index 0000000..8648d94 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:8-jre-slim +MAINTAINER xuxueli + +ENV PARAMS="" + +ENV TZ=PRC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +ADD target/xxl-job-executor-sample-springboot-*.jar /app.jar + +ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /app.jar $PARAMS"] \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml new file mode 100644 index 0000000..84ea9ea --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-executor-samples</artifactId> + <version>2.3.1</version> + </parent> + <artifactId>xxl-job-executor-sample-springboot</artifactId> + <packaging>jar</packaging> + + <name>${project.artifactId}</name> + <description>Example executor project for spring boot.</description> + <url>https://www.xuxueli.com/</url> + + <properties> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <!-- Import dependency management from Spring Boot (依赖管理:继承一些默认的依赖,工程需要依赖的jar包的管理,申明其他dependency的时候就不需要version) --> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>${spring-boot.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <!-- spring-boot-starter-web (spring-webmvc + tomcat) --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- xxl-job-core --> + <dependency> + <groupId>com.xuxueli</groupId> + <artifactId>xxl-job-core</artifactId> + <version>${project.parent.version}</version> + </dependency> + + </dependencies> + + <build> + <plugins> + <!-- spring-boot-maven-plugin (提供了直接运行项目的插件:如果是通过parent方式继承spring-boot-starter-parent则不用此插件) --> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <version>${spring-boot.version}</version> + <executions> + <execution> + <goals> + <goal>repackage</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + + +</project> \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java new file mode 100644 index 0000000..d427acc --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java @@ -0,0 +1,16 @@ +package com.xxl.job.executor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-10-28 00:38:13 + */ +@SpringBootApplication +public class XxlJobExecutorApplication { + + public static void main(String[] args) { + SpringApplication.run(XxlJobExecutorApplication.class, args); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java new file mode 100644 index 0000000..bfd80e2 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java @@ -0,0 +1,78 @@ +package com.xxl.job.executor.core.config; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ +@Configuration +public class XxlJobConfig { + private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); + + @Value("${xxl.job.admin.addresses}") + private String adminAddresses; + + @Value("${xxl.job.accessToken}") + private String accessToken; + + @Value("${xxl.job.executor.appname}") + private String appname; + + @Value("${xxl.job.executor.address}") + private String address; + + @Value("${xxl.job.executor.ip}") + private String ip; + + @Value("${xxl.job.executor.port}") + private int port; + + @Value("${xxl.job.executor.logpath}") + private String logPath; + + @Value("${xxl.job.executor.logretentiondays}") + private int logRetentionDays; + + + @Bean + public XxlJobSpringExecutor xxlJobExecutor() { + logger.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(adminAddresses); + xxlJobSpringExecutor.setAppname(appname); + xxlJobSpringExecutor.setAddress(address); + xxlJobSpringExecutor.setIp(ip); + xxlJobSpringExecutor.setPort(port); + xxlJobSpringExecutor.setAccessToken(accessToken); + xxlJobSpringExecutor.setLogPath(logPath); + xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); + + return xxlJobSpringExecutor; + } + + /** + * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP; + * + * 1、引入依赖: + * <dependency> + * <groupId>org.springframework.cloud</groupId> + * <artifactId>spring-cloud-commons</artifactId> + * <version>${version}</version> + * </dependency> + * + * 2、配置文件,或者容器启动变量 + * spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.' + * + * 3、获取IP + * String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + */ + + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/mvc/controller/IndexController.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/mvc/controller/IndexController.java new file mode 100644 index 0000000..37c9071 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/mvc/controller/IndexController.java @@ -0,0 +1,18 @@ +//package com.xxl.job.executor.mvc.controller; +// +//import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +//import org.springframework.stereotype.Controller; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.ResponseBody; +// +//@Controller +//@EnableAutoConfiguration +//public class IndexController { +// +// @RequestMapping("/") +// @ResponseBody +// String index() { +// return "xxl job executor running."; +// } +// +//} \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java new file mode 100644 index 0000000..759d662 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java @@ -0,0 +1,253 @@ +package com.xxl.job.executor.service.jobhandler; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +/** + * XxlJob开发示例(Bean模式) + * + * 开发步骤: + * 1、任务开发:在Spring Bean实例中,开发Job方法; + * 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 + * 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志; + * 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果; + * + * @author xuxueli 2019-12-11 21:52:51 + */ +@Component +public class SampleXxlJob { + private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class); + + + /** + * 1、简单任务示例(Bean模式) + */ + @XxlJob("demoJobHandler") + public void demoJobHandler() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + + for (int i = 0; i < 5; i++) { + XxlJobHelper.log("beat at:" + i); + TimeUnit.SECONDS.sleep(2); + } + // default success + } + + + /** + * 2、分片广播任务 + */ + @XxlJob("shardingJobHandler") + public void shardingJobHandler() throws Exception { + + // 分片参数 + int shardIndex = XxlJobHelper.getShardIndex(); + int shardTotal = XxlJobHelper.getShardTotal(); + + XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal); + + // 业务逻辑 + for (int i = 0; i < shardTotal; i++) { + if (i == shardIndex) { + XxlJobHelper.log("第 {} 片, 命中分片开始处理", i); + } else { + XxlJobHelper.log("第 {} 片, 忽略", i); + } + } + + } + + + /** + * 3、命令行任务 + */ + @XxlJob("commandJobHandler") + public void commandJobHandler() throws Exception { + String command = XxlJobHelper.getJobParam(); + int exitValue = -1; + + BufferedReader bufferedReader = null; + try { + // command process + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(command); + processBuilder.redirectErrorStream(true); + + Process process = processBuilder.start(); + //Process process = Runtime.getRuntime().exec(command); + + BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream()); + bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream)); + + // command log + String line; + while ((line = bufferedReader.readLine()) != null) { + XxlJobHelper.log(line); + } + + // command exit + process.waitFor(); + exitValue = process.exitValue(); + } catch (Exception e) { + XxlJobHelper.log(e); + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + + if (exitValue == 0) { + // default success + } else { + XxlJobHelper.handleFail("command exit value("+exitValue+") is failed"); + } + + } + + + /** + * 4、跨平台Http任务 + * 参数示例: + * "url: http://www.baidu.com\n" + + * "method: get\n" + + * "data: content\n"; + */ + @XxlJob("httpJobHandler") + public void httpJobHandler() throws Exception { + + // param parse + String param = XxlJobHelper.getJobParam(); + if (param==null || param.trim().length()==0) { + XxlJobHelper.log("param["+ param +"] invalid."); + + XxlJobHelper.handleFail(); + return; + } + + String[] httpParams = param.split("\n"); + String url = null; + String method = null; + String data = null; + for (String httpParam: httpParams) { + if (httpParam.startsWith("url:")) { + url = httpParam.substring(httpParam.indexOf("url:") + 4).trim(); + } + if (httpParam.startsWith("method:")) { + method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase(); + } + if (httpParam.startsWith("data:")) { + data = httpParam.substring(httpParam.indexOf("data:") + 5).trim(); + } + } + + // param valid + if (url==null || url.trim().length()==0) { + XxlJobHelper.log("url["+ url +"] invalid."); + + XxlJobHelper.handleFail(); + return; + } + if (method==null || !Arrays.asList("GET", "POST").contains(method)) { + XxlJobHelper.log("method["+ method +"] invalid."); + + XxlJobHelper.handleFail(); + return; + } + boolean isPostMethod = method.equals("POST"); + + // request + HttpURLConnection connection = null; + BufferedReader bufferedReader = null; + try { + // connection + URL realUrl = new URL(url); + connection = (HttpURLConnection) realUrl.openConnection(); + + // connection setting + connection.setRequestMethod(method); + connection.setDoOutput(isPostMethod); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setReadTimeout(5 * 1000); + connection.setConnectTimeout(3 * 1000); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); + + // do connection + connection.connect(); + + // data + if (isPostMethod && data!=null && data.trim().length()>0) { + DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); + dataOutputStream.write(data.getBytes("UTF-8")); + dataOutputStream.flush(); + dataOutputStream.close(); + } + + // valid StatusCode + int statusCode = connection.getResponseCode(); + if (statusCode != 200) { + throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid."); + } + + // result + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + String responseMsg = result.toString(); + + XxlJobHelper.log(responseMsg); + + return; + } catch (Exception e) { + XxlJobHelper.log(e); + + XxlJobHelper.handleFail(); + return; + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + if (connection != null) { + connection.disconnect(); + } + } catch (Exception e2) { + XxlJobHelper.log(e2); + } + } + + } + + /** + * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑; + */ + @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy") + public void demoJobHandler2() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + } + public void init(){ + logger.info("init"); + } + public void destroy(){ + logger.info("destroy"); + } + + +} diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties new file mode 100644 index 0000000..14c796e --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties @@ -0,0 +1,26 @@ +# web port +server.port=8081 +# no web +#spring.main.web-environment=false + +# log config +logging.config=classpath:logback.xml + + +### xxl-job admin address list, such as "http://address" or "http://address01,http://address02" +xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + +### xxl-job, access token +xxl.job.accessToken=default_token + +### xxl-job executor appname +xxl.job.executor.appname=xxl-job-executor-sample +### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null +xxl.job.executor.address= +### xxl-job executor server-info +xxl.job.executor.ip= +xxl.job.executor.port=9999 +### xxl-job executor log-path +xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler +### xxl-job executor log-retention-days +xxl.job.executor.logretentiondays=30 diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml new file mode 100644 index 0000000..d5a0d2c --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration debug="false" scan="true" scanPeriod="1 seconds"> + + <contextName>logback</contextName> + <property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/> + + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>${log.path}</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern> + </rollingPolicy> + <encoder> + <pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n + </pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="console"/> + <appender-ref ref="file"/> + </root> + +</configuration> \ No newline at end of file diff --git a/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java new file mode 100644 index 0000000..456a7d5 --- /dev/null +++ b/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java @@ -0,0 +1,14 @@ +package com.xxl.job.executor.test; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class XxlJobExecutorExampleBootApplicationTests { + + @Test + public void test() { + System.out.println(11); + } + +} \ No newline at end of file -- Gitblit v1.9.3