Repository: xuxueli/xxl-job Branch: master Commit: 1f4ba7bed77e Files: 268 Total size: 2.9 MB Directory structure: gitextract_80xsumby/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ ├── PULL_REQUEST_TEMPLATE │ └── workflows/ │ └── maven.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── doc/ │ ├── XXL-JOB-English-Documentation.md │ ├── XXL-JOB官方文档.md │ ├── XXL-JOB架构图.key │ └── db/ │ └── tables_xxl_job.sql ├── docker/ │ └── docker-compose.yml ├── pom.xml ├── xxl-job-admin/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── xxl/ │ │ │ └── job/ │ │ │ └── admin/ │ │ │ ├── XxlJobAdminApplication.java │ │ │ ├── constant/ │ │ │ │ ├── Consts.java │ │ │ │ └── TriggerStatus.java │ │ │ ├── controller/ │ │ │ │ ├── base/ │ │ │ │ │ ├── IndexController.java │ │ │ │ │ └── LoginController.java │ │ │ │ └── biz/ │ │ │ │ ├── JobCodeController.java │ │ │ │ ├── JobGroupController.java │ │ │ │ ├── JobInfoController.java │ │ │ │ ├── JobLogController.java │ │ │ │ └── JobUserController.java │ │ │ ├── mapper/ │ │ │ │ ├── XxlJobGroupMapper.java │ │ │ │ ├── XxlJobInfoMapper.java │ │ │ │ ├── XxlJobLockMapper.java │ │ │ │ ├── XxlJobLogGlueMapper.java │ │ │ │ ├── XxlJobLogMapper.java │ │ │ │ ├── XxlJobLogReportMapper.java │ │ │ │ ├── XxlJobRegistryMapper.java │ │ │ │ └── XxlJobUserMapper.java │ │ │ ├── model/ │ │ │ │ ├── XxlJobGroup.java │ │ │ │ ├── XxlJobInfo.java │ │ │ │ ├── XxlJobLog.java │ │ │ │ ├── XxlJobLogGlue.java │ │ │ │ ├── XxlJobLogReport.java │ │ │ │ ├── XxlJobRegistry.java │ │ │ │ ├── XxlJobUser.java │ │ │ │ └── dto/ │ │ │ │ └── XxlBootResourceDTO.java │ │ │ ├── scheduler/ │ │ │ │ ├── alarm/ │ │ │ │ │ ├── JobAlarm.java │ │ │ │ │ ├── JobAlarmer.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── EmailJobAlarm.java │ │ │ │ ├── complete/ │ │ │ │ │ └── JobCompleter.java │ │ │ │ ├── config/ │ │ │ │ │ └── XxlJobAdminBootstrap.java │ │ │ │ ├── cron/ │ │ │ │ │ └── CronExpression.java │ │ │ │ ├── exception/ │ │ │ │ │ └── XxlJobException.java │ │ │ │ ├── misfire/ │ │ │ │ │ ├── MisfireHandler.java │ │ │ │ │ ├── MisfireStrategyEnum.java │ │ │ │ │ └── strategy/ │ │ │ │ │ ├── MisfireDoNothing.java │ │ │ │ │ └── MisfireFireOnceNow.java │ │ │ │ ├── openapi/ │ │ │ │ │ └── OpenApiController.java │ │ │ │ ├── route/ │ │ │ │ │ ├── ExecutorRouteStrategyEnum.java │ │ │ │ │ ├── ExecutorRouter.java │ │ │ │ │ └── strategy/ │ │ │ │ │ ├── ExecutorRouteBusyover.java │ │ │ │ │ ├── ExecutorRouteConsistentHash.java │ │ │ │ │ ├── ExecutorRouteFailover.java │ │ │ │ │ ├── ExecutorRouteFirst.java │ │ │ │ │ ├── ExecutorRouteLFU.java │ │ │ │ │ ├── ExecutorRouteLRU.java │ │ │ │ │ ├── ExecutorRouteLast.java │ │ │ │ │ ├── ExecutorRouteRandom.java │ │ │ │ │ └── ExecutorRouteRound.java │ │ │ │ ├── thread/ │ │ │ │ │ ├── JobCompleteHelper.java │ │ │ │ │ ├── JobFailAlarmMonitorHelper.java │ │ │ │ │ ├── JobLogReportHelper.java │ │ │ │ │ ├── JobRegistryHelper.java │ │ │ │ │ ├── JobScheduleHelper.java │ │ │ │ │ └── JobTriggerPoolHelper.java │ │ │ │ ├── trigger/ │ │ │ │ │ ├── JobTrigger.java │ │ │ │ │ └── TriggerTypeEnum.java │ │ │ │ └── type/ │ │ │ │ ├── ScheduleType.java │ │ │ │ ├── ScheduleTypeEnum.java │ │ │ │ └── strategy/ │ │ │ │ ├── CronScheduleType.java │ │ │ │ ├── FixRateScheduleType.java │ │ │ │ └── NoneScheduleType.java │ │ │ ├── service/ │ │ │ │ ├── XxlJobService.java │ │ │ │ └── impl/ │ │ │ │ ├── AdminBizImpl.java │ │ │ │ └── XxlJobServiceImpl.java │ │ │ ├── util/ │ │ │ │ ├── I18nUtil.java │ │ │ │ ├── JobGroupPermissionUtil.java │ │ │ │ └── old/ │ │ │ │ ├── CommonDataInterceptor.java │ │ │ │ ├── CookieUtil.java │ │ │ │ ├── FtlUtil.java │ │ │ │ ├── JacksonUtil.java │ │ │ │ ├── LocalCacheUtil.java │ │ │ │ ├── RemoteHttpJobBean.java │ │ │ │ ├── XxlJobDynamicScheduler.java │ │ │ │ └── XxlJobThreadPool.java │ │ │ └── web/ │ │ │ ├── error/ │ │ │ │ ├── WebErrorPageRegistrar.java │ │ │ │ └── WebHandlerExceptionResolver.java │ │ │ └── xxlsso/ │ │ │ ├── SimpleLoginStore.java │ │ │ └── XxlSsoConfig.java │ │ └── resources/ │ │ ├── application.properties │ │ ├── i18n/ │ │ │ ├── message_en.properties │ │ │ ├── message_zh_CN.properties │ │ │ └── message_zh_TC.properties │ │ ├── logback.xml │ │ ├── mapper/ │ │ │ ├── XxlJobGroupMapper.xml │ │ │ ├── XxlJobInfoMapper.xml │ │ │ ├── XxlJobLockMapper.xml │ │ │ ├── XxlJobLogGlueMapper.xml │ │ │ ├── XxlJobLogMapper.xml │ │ │ ├── XxlJobLogReportMapper.xml │ │ │ ├── XxlJobRegistryMapper.xml │ │ │ └── XxlJobUserMapper.xml │ │ ├── static/ │ │ │ ├── adminlte/ │ │ │ │ ├── bower_components/ │ │ │ │ │ ├── bootstrap-daterangepicker/ │ │ │ │ │ │ ├── daterangepicker.css │ │ │ │ │ │ └── daterangepicker.js │ │ │ │ │ ├── ckeditor/ │ │ │ │ │ │ ├── ckeditor.js │ │ │ │ │ │ ├── config.js │ │ │ │ │ │ ├── contents.css │ │ │ │ │ │ ├── lang/ │ │ │ │ │ │ │ └── zh-cn.js │ │ │ │ │ │ ├── plugins/ │ │ │ │ │ │ │ ├── image/ │ │ │ │ │ │ │ │ └── dialogs/ │ │ │ │ │ │ │ │ └── image.js │ │ │ │ │ │ │ ├── link/ │ │ │ │ │ │ │ │ └── dialogs/ │ │ │ │ │ │ │ │ ├── anchor.js │ │ │ │ │ │ │ │ └── link.js │ │ │ │ │ │ │ ├── scayt/ │ │ │ │ │ │ │ │ ├── dialogs/ │ │ │ │ │ │ │ │ │ ├── dialog.css │ │ │ │ │ │ │ │ │ ├── options.js │ │ │ │ │ │ │ │ │ └── toolbar.css │ │ │ │ │ │ │ │ └── skins/ │ │ │ │ │ │ │ │ └── moono-lisa/ │ │ │ │ │ │ │ │ └── scayt.css │ │ │ │ │ │ │ ├── specialchar/ │ │ │ │ │ │ │ │ └── dialogs/ │ │ │ │ │ │ │ │ ├── lang/ │ │ │ │ │ │ │ │ │ └── zh-cn.js │ │ │ │ │ │ │ │ └── specialchar.js │ │ │ │ │ │ │ ├── table/ │ │ │ │ │ │ │ │ └── dialogs/ │ │ │ │ │ │ │ │ └── table.js │ │ │ │ │ │ │ ├── tableselection/ │ │ │ │ │ │ │ │ └── styles/ │ │ │ │ │ │ │ │ └── tableselection.css │ │ │ │ │ │ │ └── wsc/ │ │ │ │ │ │ │ └── skins/ │ │ │ │ │ │ │ └── moono-lisa/ │ │ │ │ │ │ │ └── wsc.css │ │ │ │ │ │ ├── skins/ │ │ │ │ │ │ │ └── moono-lisa/ │ │ │ │ │ │ │ ├── dialog.css │ │ │ │ │ │ │ ├── dialog_ie.css │ │ │ │ │ │ │ ├── dialog_ie8.css │ │ │ │ │ │ │ ├── dialog_iequirks.css │ │ │ │ │ │ │ ├── editor.css │ │ │ │ │ │ │ ├── editor_gecko.css │ │ │ │ │ │ │ ├── editor_ie.css │ │ │ │ │ │ │ ├── editor_ie8.css │ │ │ │ │ │ │ ├── editor_iequirks.css │ │ │ │ │ │ │ └── readme.md │ │ │ │ │ │ └── styles.js │ │ │ │ │ ├── fastclick/ │ │ │ │ │ │ └── fastclick.js │ │ │ │ │ └── font-awesome/ │ │ │ │ │ └── fonts/ │ │ │ │ │ └── FontAwesome.otf │ │ │ │ └── plugins/ │ │ │ │ └── iCheck/ │ │ │ │ └── square/ │ │ │ │ └── blue.css │ │ │ ├── biz/ │ │ │ │ └── common/ │ │ │ │ ├── admin.setting.js │ │ │ │ ├── admin.tab.css │ │ │ │ ├── admin.tab.js │ │ │ │ ├── admin.table.js │ │ │ │ └── admin.util.js │ │ │ └── plugins/ │ │ │ ├── codemirror/ │ │ │ │ ├── addon/ │ │ │ │ │ └── hint/ │ │ │ │ │ ├── anyword-hint.js │ │ │ │ │ ├── show-hint.css │ │ │ │ │ └── show-hint.js │ │ │ │ ├── lib/ │ │ │ │ │ ├── codemirror.css │ │ │ │ │ └── codemirror.js │ │ │ │ └── mode/ │ │ │ │ ├── clike/ │ │ │ │ │ └── clike.js │ │ │ │ ├── javascript/ │ │ │ │ │ └── javascript.js │ │ │ │ ├── php/ │ │ │ │ │ └── php.js │ │ │ │ ├── powershell/ │ │ │ │ │ └── powershell.js │ │ │ │ ├── python/ │ │ │ │ │ └── python.js │ │ │ │ └── shell/ │ │ │ │ └── shell.js │ │ │ ├── cronGen/ │ │ │ │ ├── cronGen.js │ │ │ │ └── cronGen_en.js │ │ │ ├── fullscreen/ │ │ │ │ └── jquery.fullscreen.js │ │ │ ├── jquery-treegrid/ │ │ │ │ └── jquery.treegrid.css │ │ │ ├── layer/ │ │ │ │ ├── layer.js │ │ │ │ └── theme/ │ │ │ │ └── default/ │ │ │ │ └── layer.css │ │ │ ├── nprogress/ │ │ │ │ ├── nprogress.css │ │ │ │ └── nprogress.js │ │ │ └── zTree/ │ │ │ ├── css/ │ │ │ │ └── metroStyle/ │ │ │ │ └── metroStyle.css │ │ │ └── js/ │ │ │ ├── jquery.ztree.core.js │ │ │ └── jquery.ztree.excheck.js │ │ └── templates/ │ │ ├── base/ │ │ │ ├── dashboard.ftl │ │ │ ├── help.ftl │ │ │ ├── index.ftl │ │ │ └── login.ftl │ │ ├── biz/ │ │ │ ├── group.list.ftl │ │ │ ├── job.code.ftl │ │ │ ├── job.list.ftl │ │ │ ├── log.detail.ftl │ │ │ ├── log.list.ftl │ │ │ └── user.list.ftl │ │ └── common/ │ │ ├── common.errorpage.ftl │ │ └── common.macro.ftl │ └── test/ │ └── java/ │ └── com/ │ └── xxl/ │ └── job/ │ ├── admin/ │ │ ├── controller/ │ │ │ ├── AbstractSpringMvcTest.java │ │ │ └── JobInfoControllerTest.java │ │ ├── core/ │ │ │ └── util/ │ │ │ ├── CronExpressionTest.java │ │ │ └── JacksonUtilTest.java │ │ ├── mapper/ │ │ │ ├── XxlJobGroupMapperTest.java │ │ │ ├── XxlJobInfoMapperTest.java │ │ │ ├── XxlJobLogGlueMapperTest.java │ │ │ ├── XxlJobLogMapperTest.java │ │ │ ├── XxlJobLogReportMapperTest.java │ │ │ └── XxlJobRegistryMapperTest.java │ │ ├── schedule/ │ │ │ └── JobScheduleTest.java │ │ └── util/ │ │ └── I18nUtilTest.java │ └── openapi/ │ ├── AdminBizTest.java │ └── ExecutorBizTest.java ├── xxl-job-core/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── xxl/ │ └── job/ │ └── core/ │ ├── constant/ │ │ ├── Const.java │ │ ├── ExecutorBlockStrategyEnum.java │ │ └── RegistType.java │ ├── context/ │ │ ├── XxlJobContext.java │ │ └── XxlJobHelper.java │ ├── executor/ │ │ ├── XxlJobExecutor.java │ │ └── impl/ │ │ ├── XxlJobSimpleExecutor.java │ │ └── XxlJobSpringExecutor.java │ ├── glue/ │ │ ├── GlueFactory.java │ │ ├── GlueTypeEnum.java │ │ └── impl/ │ │ └── SpringGlueFactory.java │ ├── handler/ │ │ ├── IJobHandler.java │ │ ├── annotation/ │ │ │ ├── JobHandler.java │ │ │ └── XxlJob.java │ │ └── impl/ │ │ ├── GlueJobHandler.java │ │ ├── MethodJobHandler.java │ │ └── ScriptJobHandler.java │ ├── log/ │ │ └── XxlJobFileAppender.java │ ├── openapi/ │ │ ├── AdminBiz.java │ │ ├── ExecutorBiz.java │ │ ├── impl/ │ │ │ └── ExecutorBizImpl.java │ │ └── model/ │ │ ├── CallbackRequest.java │ │ ├── IdleBeatRequest.java │ │ ├── KillRequest.java │ │ ├── LogRequest.java │ │ ├── LogResult.java │ │ ├── RegistryRequest.java │ │ └── TriggerRequest.java │ ├── server/ │ │ └── EmbedServer.java │ ├── thread/ │ │ ├── ExecutorRegistryThread.java │ │ ├── JobLogFileCleanThread.java │ │ ├── JobThread.java │ │ └── TriggerCallbackThread.java │ └── util/ │ ├── ScriptUtil.java │ └── deprecated/ │ ├── AdminBizClient.java │ ├── DateUtil.java │ ├── ExecutorBizClient.java │ ├── FileUtil.java │ ├── GsonTool.java │ ├── IpUtil.java │ ├── JdkSerializeTool.java │ ├── NetUtil.java │ ├── ReturnT.java │ ├── ShardingUtil.java │ ├── ThrowableUtil.java │ └── XxlJobRemotingUtil.java └── xxl-job-executor-samples/ ├── pom.xml ├── xxl-job-executor-sample-frameless/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── xxl/ │ │ │ └── job/ │ │ │ └── executor/ │ │ │ └── sample/ │ │ │ └── frameless/ │ │ │ ├── XxlJobFramelessApplication.java │ │ │ ├── config/ │ │ │ │ └── FrameLessXxlJobConfig.java │ │ │ └── jobhandler/ │ │ │ └── SampleXxlJob.java │ │ └── resources/ │ │ ├── log4j.xml │ │ └── xxl-job-executor.properties │ └── test/ │ └── java/ │ └── com/ │ └── xxl/ │ └── job/ │ └── executor/ │ └── sample/ │ └── frameless/ │ └── test/ │ └── FramelessApplicationTest.java ├── xxl-job-executor-sample-springboot/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── xxl/ │ │ │ └── job/ │ │ │ └── executor/ │ │ │ ├── XxlJobExecutorApplication.java │ │ │ ├── config/ │ │ │ │ └── XxlJobConfig.java │ │ │ ├── controller/ │ │ │ │ └── IndexController.java │ │ │ └── jobhandler/ │ │ │ └── SampleXxlJob.java │ │ └── resources/ │ │ ├── application.properties │ │ └── logback.xml │ └── test/ │ └── java/ │ └── com/ │ └── xxl/ │ └── job/ │ └── executor/ │ └── test/ │ └── XxlJobExecutorExampleBootApplicationTests.java └── xxl-job-executor-sample-springboot-ai/ ├── Dockerfile ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── xxl/ │ │ └── job/ │ │ └── executor/ │ │ ├── XxlJobAIExecutorApplication.java │ │ ├── config/ │ │ │ └── XxlJobConfig.java │ │ ├── controller/ │ │ │ └── IndexController.java │ │ └── jobhandler/ │ │ └── AIXxlJob.java │ └── resources/ │ ├── application.properties │ └── logback.xml └── test/ └── java/ └── com/ └── xxl/ └── job/ └── executor/ └── test/ ├── BaseTests.java ├── dify/ │ └── DifyTest.java └── ollama/ └── OllamaTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://www.xuxueli.com/page/donate.html'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE ================================================ Please answer some questions before submitting your issue. Thanks! ### Which version of XXL-JOB do you using? ### Expected behavior ### Actual behavior ### Steps to reproduce the behavior ### Other information ================================================ FILE: .github/PULL_REQUEST_TEMPLATE ================================================ **What kind of change does this PR introduce?** (check at least one) - [ ] Bugfix - [ ] Feature - [ ] Code style update - [ ] Refactor - [ ] Build-related changes - [ ] Other, please describe: **The description of the PR:** **Other information:** ================================================ FILE: .github/workflows/maven.yml ================================================ name: Java CI on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn -B package --file pom.xml ================================================ FILE: .gitignore ================================================ .idea .classpath .project *.iml target/ .DS_Store .gitattributes ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: NOTICE ================================================ Copyright (c) 2015-present, xuxueli. Dependencies: ================================================================ Spring: * LICENSE: * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) * HOMEPAGE: * http://www.springsource.org Netty: * LICENSE: * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) * HOMEPAGE: * https://github.com/netty/netty Mybatis: * LICENSE: * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) * HOMEPAGE: * https://mybatis.org/mybatis-3/ SLF4J: * LICENSE: * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) * HOMEPAGE: * http://www.slf4j.org ================================================ FILE: README.md ================================================

XXL-JOB

XXL-JOB, a distributed task scheduling framework.
-- Home Page --

## Introduction XXL-JOB is a distributed task scheduling framework. It's core design goal is to develop quickly and learn simple, lightweight, and easy to expand. Now, it's already open source, and many companies use it in production environments, real "out-of-the-box". XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 ## Sponsor XXL-JOB is an open source and free project, with its ongoing development made possible entirely by the support of these awesome backers. XXL-JOB 是一个开源且免费项目,其正在进行的开发完全得益于支持者的支持。开源不易,[前往赞助项目开发](https://www.xuxueli.com/page/donate.html )

金牌赞助方


阿里云 提供云上托管 XXL-JOB
## Documentation - [中文文档](https://www.xuxueli.com/xxl-job/) - [English Documentation](https://www.xuxueli.com/xxl-job/en/) ## Communication - [社区交流](https://www.xuxueli.com/page/community.html) ## Features - 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、跨语言/OpenAPI:调度中心与执行器提供语言无关的 OpenApi(RESTful 格式),第三方任意语言可据此对接调度中心或者实现执行器,实现多语言支持。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案; - 32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文; - 33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; - 34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; - 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; - 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; - 37、AI任务:原生提供AI执行器,并内置多个AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。 - 38、审计日志:记录任务操作敏感信息,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题。 - 39、优雅停机:调度中心停机,检测时间轮非空时主动等待调度完成;客户端停机,检测存在运行中任务时,停止接收新任务并主动等待任务执行完成; ## Development 于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) "评比,在当时已录入的一万多个开源项目中角逐,最终当选"最受欢迎项目"。 于2024-11-06,XXL-JOB经 GitCode 官方评审,获得 “G-Star项目毕业认证”。 > 我司大众点评目前已接入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、北京盛哲科技有限公司 - 558、广州钛动科技有限公司 - 559、北京大禹汇智科技有限公司 - 560、湖南鼎翰文化股份有限公司 - 561、苏州安软信息科技有限公司 - 562、芒果tv - 563、上海艺赛旗软件股份有限公司 - 564、中盈优创资讯科技有限公司 - 565、乐乎公寓 - 566、启明信息 - 567、苏州安软 - 568、南京富金的软件科技有限公司 - 569、深圳市新科聚合网络技术有限公司 - 570、你好现在(北京)科技股份有限公司 - 571、360考试宝典 - 572、北京一零科技有限公司 - 573、厦门星纵信息 - 574、Dalligent Solusi Indonesia - 575、深圳华普物联科技有限公司 - 576、深圳行健自动化股份有限公司 - 577、深圳市富融信息科技服务有限公司 - 578、蓝鸟云 - 579、上海澎博财经资讯有限公司 - 580、北京小鸦科技有限公司 - 581、杭州盈泉云科技有限公司 - 582、惟客数据 - 583、GOSO香蜜闺秀 - 584、普乐师(上海)数字科技有限公司 - 585、西安市雁塔区咖北堂网络科技部 - 586、宁波聚臻智能科技有限公司 - 587、普乐师数字科技有限公司 - 588、江苏蟹联网科技有限公司 - 589、杭州未智科技有限公司 - 590、安吉智行物流有限公司 - 591、华生大家居集团有限公司 - 592、美心食品(广州)有限公司 - 593、货拉拉【货拉拉APP】 - 594、杭州思韬瑞科技有限公司 - 595、杭州玖融科技有限公司 - 596、北京优海网络科技有限公司 - 597、浙江大维高新技术股份有限公司 - 598、粤港澳大湾区数字经济研究院 - 599、普康(杭州)健康科技有限公司 - 600、华西证券股份有限公司【华西证券】 - 601、杭州海康机器人股份有限公司【海康】 - 602、河南宸邦信息技术有限公司 - 603、成都次元节点网络科技有限公司 - 604、富士康科技集团【富士康】 - 605、青岛东软载波科技股份有限公司 - 606、小菊快跑科技有限公司 - 607、视源股份 - 608、宁波聚臻智能科技有限公司 - 609、阔天科技有限公司 - 610、网宿科技有限公司 - 611、南京梵鼎信息技术有限公司 - 612、房天下【房天下】 - 613、特瓦特能源科技有限公司 - 614、拓迪智能科技有限公司 - 615、东软集团【东软】 - 616、开普云 - 617、领课网络 - 618、南京特维软件有限公司 - 619、福建易联众保睿通信息科技有限公司 - 620、浙江核心同花顺金融科技有限公司【同花顺】 - 621、浙江博观瑞思科技有限公司 - 622、北京新美互通科技有限公司 - 623、北京有生博大软件股份有限公司 - 624、时代中国 - 625、鱼泡网 - 626、一粒方糖(安徽)科技有限公司 - 627、北京外研在线数字科技有限公司 - 628、德电(中国)通信技术有限公司 - 629、杭州寻联网络科技有限公司 - 630、橙联(中国)有限公司 - 631、北京承启通科技有限公司 - 632、银联数据服务有限公司【银联】 - 633、上海晶确科技有限公司 - 634、亚信科技有限公司 - 635、福建新航物联网科技有限公司 - 636、上扬软件 - 637、深蓝汽车科技有限公司 - 638、南昌节点汇智科技有限公司 - 639、锐明技术 - 640、再造再生健康科技有限公司 - 641、华宝证券 - 642、卓正医疗 - 643、深圳湛信科技 - 644、陕西鑫众为软件有限公司 - 645、深圳市润农科技有限公司 - 646、庚商教育智能科技有限公司 - 647、杭州祎声科技 - 648、四川久远银海软件股份有限公司 - 649、GeeFox极狐低代码 - 650、浙江和仁科技股份有限公司 - 651、宁波聚臻智能科技有限公司 - 652、福建福昕软件开发股份有限公司【福昕】 - 653、广州中长康达信息技术有限公司 - 654、武汉趣改信息科技有限公司 - 655、北京华夏思源科技发展有限公司 - 656、宁波关关通科技有限公司 - 657、青岛吕氏餐饮有限公司 - 658、杭州乐刻网络科技有限公司 - 659、上海红瓦信息科技有限公司 - 660、陕西旅小宝信息科技有限公司 - 661、中科卓恒(大连)科技有限公司 - 662、北京华益精点生物技术有限公司 - 663、马士基(中国)航运有限公司【马士基】 - 664、陕西美咚网络科技有限公司 - 665、山东新北洋信息技术股份有限公司 - 666、福建中瑞文化发展集团有限公司 - 667、黑龙江省建工集团有限责任公司【黑龙江省建工】 - 668、志信能达安全科技(广州)有限公司 - 669、重庆开源共创科技有限公司 - 670、华泰人寿保险股份有限公司【华泰人寿】 - 671、成都盘古纵横集团 - 672、北京果果乐学科技有限公司 - 673、北京凌云空间科技有限公司 - 674、临工重机股份有限公司 - 675、上海热风时尚管理集团【热风】 - 676、HashKey Exchange - 677、傲基(深圳)跨境商务股份有限公司 - 678、青岛文达通科技股份有限公司 - 679、杭州普罗云科技有限公司 - 680、浙江云鹭科技有限公司 - 681、中山市芯宏柿网络科技有限公司 - 682、深圳市家家顺物联科技 - 683、重庆斑西科技有限公司 - 684、福建省泰古信息技术有限公司 - 685、贵阳永青仪电科技有限公司 - 686、广州博依特智能信息科技有限公司 - 687、河南宠呦呦信息技术有限公司 - 688、陕西星邑空间技术有限公司 - 689、广东西欧克实业有限公司 - 690、唱吧麦颂KTV - 691、联通云 - 692、北京爱话本科技有限公司 - 693、北京起创科技有限公司 - 694、平安证券【平安证券】 - 695、合肥中科类脑智能技术有限公司 - 696、南京同仁堂健康产业有限公司【同仁堂】 - 697、铜仁市碧江区智惠加油站 - 698、惟客数据 - 699、凤凰新闻【凤凰新闻】 - 700、深圳王力智能 - 701、返利网数字科技股份有限公司 - 702、上海阜能信息科技有限公司 - 703、深圳市极能超电数字科技有限公司 - 704、海目星激光科技集团股份有限公司 - 705、安克创新科技股份有限公司【安克】 - 706、大庆点神科技有限公司 - 707、浙江零跑科技股份有限公司【零跑】 - 708、成都成电金盘健康数据技术有限公司 - 709、成都极米科技股份有限公司【极米】 - 710、顺德职业技术大学 - 711、中邮证券有限责任公司【中邮证券】 - 712、志豪链云科技有限公司 - 713、湖南万鲸科技有限公司 - 714、广州万表 - 715、再惠(上海)网络科技有限公司 - 716、上海爱诚裕信息科技有限公司 - 717、杭州迈瑞数字科技有限公司 - 718、广州串联网络科技有限公司 - …… > 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 欢迎大家的关注和使用,XXL-JOB也将拥抱变化,持续发展。 ## 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. 欢迎参与项目贡献!比如提交PR修复一个bug,或者新建 [Issue](https://github.com/xuxueli/xxl-job/issues/) 讨论新特性或者变更。 ## 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. 产品开源免费,并且将持续提供免费的社区技术支持。个人或企业内部可自由的接入和使用。如有需要可 [邮件联系](https://www.xuxueli.com/page/community.html) 作者免费获取项目授权。 ================================================ FILE: doc/XXL-JOB-English-Documentation.md ================================================ ## 《Distributed task scheduling framework XXL-JOB》 [![Build Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions) [![Maven Central](https://img.shields.io/maven-central/v/com.xuxueli/xxl-job-core)](https://central.sonatype.com/artifact/com.xuxueli/xxl-job-core) [![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 pulls](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、猿辅导【猿辅导】 - 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、北京盛哲科技有限公司 - 558、广州钛动科技有限公司 - 559、北京大禹汇智科技有限公司 - 560、湖南鼎翰文化股份有限公司 - 561、苏州安软信息科技有限公司 - 562、芒果tv - 563、上海艺赛旗软件股份有限公司 - 564、中盈优创资讯科技有限公司 - 565、乐乎公寓 - 566、启明信息 - 567、苏州安软 - 568、南京富金的软件科技有限公司 - 569、深圳市新科聚合网络技术有限公司 - 570、你好现在(北京)科技股份有限公司 - 571、360考试宝典 - 572、北京一零科技有限公司 - 573、厦门星纵信息 - 574、Dalligent Solusi Indonesia - 575、深圳华普物联科技有限公司 - 576、深圳行健自动化股份有限公司 - 577、深圳市富融信息科技服务有限公司 - 578、蓝鸟云 - 579、上海澎博财经资讯有限公司 - 580、北京小鸦科技有限公司 - 581、杭州盈泉云科技有限公司 - 582、惟客数据 - 583、GOSO香蜜闺秀 - 584、普乐师(上海)数字科技有限公司 - 585、西安市雁塔区咖北堂网络科技部 - 586、宁波聚臻智能科技有限公司 - 587、普乐师数字科技有限公司 - 588、江苏蟹联网科技有限公司 - 589、杭州未智科技有限公司 - 590、安吉智行物流有限公司 - 591、华生大家居集团有限公司 - 592、美心食品(广州)有限公司 - 593、货拉拉【货拉拉APP】 - 594、杭州思韬瑞科技有限公司 - 595、杭州玖融科技有限公司 - 596、北京优海网络科技有限公司 - 597、浙江大维高新技术股份有限公司 - 598、粤港澳大湾区数字经济研究院 - 599、普康(杭州)健康科技有限公司 - 600、华西证券股份有限公司【华西证券】 - 601、杭州海康机器人股份有限公司【海康】 - 602、河南宸邦信息技术有限公司 - 603、成都次元节点网络科技有限公司 - 604、富士康科技集团【富士康】 - 605、青岛东软载波科技股份有限公司 - 606、小菊快跑科技有限公司 - 607、视源股份 - 608、宁波聚臻智能科技有限公司 - 609、阔天科技有限公司 - 610、网宿科技有限公司 - 611、南京梵鼎信息技术有限公司 - 612、房天下【房天下】 - 613、特瓦特能源科技有限公司 - 614、拓迪智能科技有限公司 - 615、东软集团【东软】 - 616、开普云 - 617、领课网络 - 618、南京特维软件有限公司 - 619、福建易联众保睿通信息科技有限公司 - 620、浙江核心同花顺金融科技有限公司【同花顺】 - 621、浙江博观瑞思科技有限公司 - 622、北京新美互通科技有限公司 - 623、北京有生博大软件股份有限公司 - 624、时代中国 - 625、鱼泡网 - 626、一粒方糖(安徽)科技有限公司 - 627、北京外研在线数字科技有限公司 - 628、德电(中国)通信技术有限公司 - 629、杭州寻联网络科技有限公司 - 630、橙联(中国)有限公司 - 631、北京承启通科技有限公司 - 632、银联数据服务有限公司【银联】 - 633、上海晶确科技有限公司 - 634、亚信科技有限公司 - 635、福建新航物联网科技有限公司 - 636、上扬软件 - 637、深蓝汽车科技有限公司 - 638、南昌节点汇智科技有限公司 - 639、锐明技术 - 640、再造再生健康科技有限公司 - 641、华宝证券 - 642、卓正医疗 - 643、深圳湛信科技 - 644、陕西鑫众为软件有限公司 - 645、深圳市润农科技有限公司 - 646、庚商教育智能科技有限公司 - 647、杭州祎声科技 - 648、四川久远银海软件股份有限公司 - 649、GeeFox极狐低代码 - 650、浙江和仁科技股份有限公司 - 651、宁波聚臻智能科技有限公司 - 652、福建福昕软件开发股份有限公司【福昕】 - 653、广州中长康达信息技术有限公司 - 654、武汉趣改信息科技有限公司 - 655、北京华夏思源科技发展有限公司 - 656、宁波关关通科技有限公司 - 657、青岛吕氏餐饮有限公司 - 658、杭州乐刻网络科技有限公司 - 659、上海红瓦信息科技有限公司 - 660、陕西旅小宝信息科技有限公司 - 661、中科卓恒(大连)科技有限公司 - 662、北京华益精点生物技术有限公司 - 663、马士基(中国)航运有限公司【马士基】 - 664、陕西美咚网络科技有限公司 - 665、山东新北洋信息技术股份有限公司 - 666、福建中瑞文化发展集团有限公司 - 667、黑龙江省建工集团有限责任公司【黑龙江省建工】 - 668、志信能达安全科技(广州)有限公司 - 669、重庆开源共创科技有限公司 - 670、华泰人寿保险股份有限公司【华泰人寿】 - 671、成都盘古纵横集团 - 672、北京果果乐学科技有限公司 - 673、北京凌云空间科技有限公司 - 674、临工重机股份有限公司 - 675、上海热风时尚管理集团【热风】 - 676、HashKey Exchange - 677、傲基(深圳)跨境商务股份有限公司 - 678、青岛文达通科技股份有限公司 - 679、杭州普罗云科技有限公司 - 680、浙江云鹭科技有限公司 - 681、中山市芯宏柿网络科技有限公司 - 682、深圳市家家顺物联科技 - 683、重庆斑西科技有限公司 - 684、福建省泰古信息技术有限公司 - 685、贵阳永青仪电科技有限公司 - 686、广州博依特智能信息科技有限公司 - 687、河南宠呦呦信息技术有限公司 - 688、陕西星邑空间技术有限公司 - 689、广东西欧克实业有限公司 - 690、唱吧麦颂KTV - 691、联通云 - 692、北京爱话本科技有限公司 - 693、北京起创科技有限公司 - 694、平安证券【平安证券】 - 695、合肥中科类脑智能技术有限公司 - 696、南京同仁堂健康产业有限公司【同仁堂】 - 697、铜仁市碧江区智惠加油站 - 698、惟客数据 - 699、凤凰新闻【凤凰新闻】 - 700、深圳王力智能 - 701、返利网数字科技股份有限公司 - 702、上海阜能信息科技有限公司 - 703、深圳市极能超电数字科技有限公司 - 704、海目星激光科技集团股份有限公司 - 705、安克创新科技股份有限公司【安克】 - 706、大庆点神科技有限公司 - 707、浙江零跑科技股份有限公司【零跑】 - 708、成都成电金盘健康数据技术有限公司 - 709、成都极米科技股份有限公司【极米】 - 710、顺德职业技术大学 - 711、中邮证券有限责任公司【中邮证券】 - 712、志豪链云科技有限公司 - 713、湖南万鲸科技有限公司 - 714、广州万表 - 715、再惠(上海)网络科技有限公司 - 716、上海爱诚裕信息科技有限公司 - 717、杭州迈瑞数字科技有限公司 - 718、广州串联网络科技有限公司 - …… > 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) ``` com.xuxueli xxl-job-core ${version} ``` #### Technical exchange group - [社区交流](https://www.xuxueli.com/page/community.html) - [Gitter](https://gitter.im/xuxueli/xxl-job) ### 1.5 Environment - JDK:1.8+ - Mysql:5.7+ - 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: ``` ``` #### 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.openapi.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: ``` com.xuxueli xxl-job-core ${最新稳定版} ``` - 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; ### 6.43 version v3.3.2 Release Notes[2026-01-01] - 1、【Optimization】Graceful Shutdown: When the scheduling center shuts down, it actively waits for scheduling completion when detecting non-empty time wheels; when the client shuts down, it stops receiving new tasks and actively waits for task execution completion when detecting running tasks; - 2、【New Feature】Docker Compose Configuration: Added Docker Compose configuration to support one-click configuration and startup of scheduling center clusters;
Docker Compose startup steps: ``` // Download XXL-JOB git clone --branch "$(curl -s https://api.github.com/repos/xuxueli/xxl-job/releases/latest | jq -r .tag_name)" https://github.com/xuxueli/xxl-job.git // Build XXL-JOB mvn clean package -Dmaven.test.skip=true // Start XXL-JOB MYSQL_PATH={自定义数据库持久化目录} docker compose up -d // Stop XXL-JOB docker compose down ```
- 3、【Optimization】 Scheduling Center Operation Experience: Table interaction adjusted to single-row selection mode; disabled pagination cycling; optimized pagination limit text; - 4、【Optimization】 Scheduling Thread Transaction Commit Logic: Adjusted to avoid thread abnormal exit under edge conditions, enhancing robustness; - 5、【Optimization】 Scheduling Log List Sorting Logic: Optimized for improved readability; - 6、【Optimization】 Scheduling Center OpenAPI Communication Token: Adjusted to optional instead of required; merged PR-3892; - 7、【Optimization】 Executor Detail Interface Permissions: Adjusted to support regular users viewing registered nodes; merged PR-3882; - 8、【Optimization】 Task Parameter LogDateTime Generation Logic: Adjusted to ensure consistency for the same batch of scheduling in sharding broadcast scenarios; - 9、【Upgrade】 Maven Dependencies: Upgraded multiple dependencies to newer versions, such as spring, netty, xxl-sso, xxl-tool, etc.; - 10、【Optimization】 Unified Project Dependency Management Structure: Unified dependency versions to parent pom for improved maintainability; ## 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 ) ================================================ FILE: doc/XXL-JOB官方文档.md ================================================ ## 《分布式任务调度平台XXL-JOB》 [![Build Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions) [![Maven Central](https://img.shields.io/maven-central/v/com.xuxueli/xxl-job-core)](https://central.sonatype.com/artifact/com.xuxueli/xxl-job-core) [![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 pulls](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、跨语言/OpenAPI:调度中心与执行器提供语言无关的 OpenApi(RESTful 格式),第三方任意语言可据此对接调度中心或者实现执行器,实现多语言支持。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案; - 32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文; - 33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; - 34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; - 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; - 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; - 37、AI任务:原生提供AI执行器,并内置多个AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。 - 38、审计日志:记录任务操作敏感信息,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题。 - 39、优雅停机:调度中心停机,检测时间轮非空时主动等待调度完成;客户端停机,检测存在运行中任务时,停止接收新任务并主动等待任务执行完成; ### 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、北京盛哲科技有限公司 - 558、广州钛动科技有限公司 - 559、北京大禹汇智科技有限公司 - 560、湖南鼎翰文化股份有限公司 - 561、苏州安软信息科技有限公司 - 562、芒果tv - 563、上海艺赛旗软件股份有限公司 - 564、中盈优创资讯科技有限公司 - 565、乐乎公寓 - 566、启明信息 - 567、苏州安软 - 568、南京富金的软件科技有限公司 - 569、深圳市新科聚合网络技术有限公司 - 570、你好现在(北京)科技股份有限公司 - 571、360考试宝典 - 572、北京一零科技有限公司 - 573、厦门星纵信息 - 574、Dalligent Solusi Indonesia - 575、深圳华普物联科技有限公司 - 576、深圳行健自动化股份有限公司 - 577、深圳市富融信息科技服务有限公司 - 578、蓝鸟云 - 579、上海澎博财经资讯有限公司 - 580、北京小鸦科技有限公司 - 581、杭州盈泉云科技有限公司 - 582、惟客数据 - 583、GOSO香蜜闺秀 - 584、普乐师(上海)数字科技有限公司 - 585、西安市雁塔区咖北堂网络科技部 - 586、宁波聚臻智能科技有限公司 - 587、普乐师数字科技有限公司 - 588、江苏蟹联网科技有限公司 - 589、杭州未智科技有限公司 - 590、安吉智行物流有限公司 - 591、华生大家居集团有限公司 - 592、美心食品(广州)有限公司 - 593、货拉拉【货拉拉APP】 - 594、杭州思韬瑞科技有限公司 - 595、杭州玖融科技有限公司 - 596、北京优海网络科技有限公司 - 597、浙江大维高新技术股份有限公司 - 598、粤港澳大湾区数字经济研究院 - 599、普康(杭州)健康科技有限公司 - 600、华西证券股份有限公司【华西证券】 - 601、杭州海康机器人股份有限公司【海康】 - 602、河南宸邦信息技术有限公司 - 603、成都次元节点网络科技有限公司 - 604、富士康科技集团【富士康】 - 605、青岛东软载波科技股份有限公司 - 606、小菊快跑科技有限公司 - 607、视源股份 - 608、宁波聚臻智能科技有限公司 - 609、阔天科技有限公司 - 610、网宿科技有限公司 - 611、南京梵鼎信息技术有限公司 - 612、房天下【房天下】 - 613、特瓦特能源科技有限公司 - 614、拓迪智能科技有限公司 - 615、东软集团【东软】 - 616、开普云 - 617、领课网络 - 618、南京特维软件有限公司 - 619、福建易联众保睿通信息科技有限公司 - 620、浙江核心同花顺金融科技有限公司【同花顺】 - 621、浙江博观瑞思科技有限公司 - 622、北京新美互通科技有限公司 - 623、北京有生博大软件股份有限公司 - 624、时代中国 - 625、鱼泡网 - 626、一粒方糖(安徽)科技有限公司 - 627、北京外研在线数字科技有限公司 - 628、德电(中国)通信技术有限公司 - 629、杭州寻联网络科技有限公司 - 630、橙联(中国)有限公司 - 631、北京承启通科技有限公司 - 632、银联数据服务有限公司【银联】 - 633、上海晶确科技有限公司 - 634、亚信科技有限公司 - 635、福建新航物联网科技有限公司 - 636、上扬软件 - 637、深蓝汽车科技有限公司 - 638、南昌节点汇智科技有限公司 - 639、锐明技术 - 640、再造再生健康科技有限公司 - 641、华宝证券 - 642、卓正医疗 - 643、深圳湛信科技 - 644、陕西鑫众为软件有限公司 - 645、深圳市润农科技有限公司 - 646、庚商教育智能科技有限公司 - 647、杭州祎声科技 - 648、四川久远银海软件股份有限公司 - 649、GeeFox极狐低代码 - 650、浙江和仁科技股份有限公司 - 651、宁波聚臻智能科技有限公司 - 652、福建福昕软件开发股份有限公司【福昕】 - 653、广州中长康达信息技术有限公司 - 654、武汉趣改信息科技有限公司 - 655、北京华夏思源科技发展有限公司 - 656、宁波关关通科技有限公司 - 657、青岛吕氏餐饮有限公司 - 658、杭州乐刻网络科技有限公司 - 659、上海红瓦信息科技有限公司 - 660、陕西旅小宝信息科技有限公司 - 661、中科卓恒(大连)科技有限公司 - 662、北京华益精点生物技术有限公司 - 663、马士基(中国)航运有限公司【马士基】 - 664、陕西美咚网络科技有限公司 - 665、山东新北洋信息技术股份有限公司 - 666、福建中瑞文化发展集团有限公司 - 667、黑龙江省建工集团有限责任公司【黑龙江省建工】 - 668、志信能达安全科技(广州)有限公司 - 669、重庆开源共创科技有限公司 - 670、华泰人寿保险股份有限公司【华泰人寿】 - 671、成都盘古纵横集团 - 672、北京果果乐学科技有限公司 - 673、北京凌云空间科技有限公司 - 674、临工重机股份有限公司 - 675、上海热风时尚管理集团【热风】 - 676、HashKey Exchange - 677、傲基(深圳)跨境商务股份有限公司 - 678、青岛文达通科技股份有限公司 - 679、杭州普罗云科技有限公司 - 680、浙江云鹭科技有限公司 - 681、中山市芯宏柿网络科技有限公司 - 682、深圳市家家顺物联科技 - 683、重庆斑西科技有限公司 - 684、福建省泰古信息技术有限公司 - 685、贵阳永青仪电科技有限公司 - 686、广州博依特智能信息科技有限公司 - 687、河南宠呦呦信息技术有限公司 - 688、陕西星邑空间技术有限公司 - 689、广东西欧克实业有限公司 - 690、唱吧麦颂KTV - 691、联通云 - 692、北京爱话本科技有限公司 - 693、北京起创科技有限公司 - 694、平安证券【平安证券】 - 695、合肥中科类脑智能技术有限公司 - 696、南京同仁堂健康产业有限公司【同仁堂】 - 697、铜仁市碧江区智惠加油站 - 698、惟客数据 - 699、凤凰新闻【凤凰新闻】 - 700、深圳王力智能 - 701、返利网数字科技股份有限公司 - 702、上海阜能信息科技有限公司 - 703、深圳市极能超电数字科技有限公司 - 704、海目星激光科技集团股份有限公司 - 705、安克创新科技股份有限公司【安克】 - 706、大庆点神科技有限公司 - 707、浙江零跑科技股份有限公司【零跑】 - 708、成都成电金盘健康数据技术有限公司 - 709、成都极米科技股份有限公司【极米】 - 710、顺德职业技术大学 - 711、中邮证券有限责任公司【中邮证券】 - 712、志豪链云科技有限公司 - 713、湖南万鲸科技有限公司 - 714、广州万表 - 715、再惠(上海)网络科技有限公司 - 716、上海爱诚裕信息科技有限公司 - 717、杭州迈瑞数字科技有限公司 - 718、广州串联网络科技有限公司 - …… > 更多接入的公司,欢迎在 [登记地址](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) [https://gitcode.com/xuxueli/xxl-job](https://gitcode.com/xuxueli/xxl-job) | [Download](https://gitcode.com/xuxueli/xxl-job/tags) #### 中央仓库地址 ``` com.xuxueli xxl-job-core ${最新稳定版本} ``` ### 1.6 环境 - Maven:3+ - Jdk:17+ (说明:版本3.x及以上要求Jdk17+;版本2.x及以下支持Jdk1.8) - Mysql:8.0+ ## 二、快速入门 ### 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= ### 调度中心通讯超时时间[选填],单位秒;默认3s; xxl.job.timeout=3 ### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文; xxl.job.i18n=zh_CN ## 调度线程池最大线程配置【必填】 xxl.job.triggerpool.fast.max=300 xxl.job.triggerpool.slow.max=200 ### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于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:{指定版本} ``` - 创建容器并运行 ``` /** * 如需自定义 “项目配置文件” 中配置项,比如 mysql 配置,可通过 "-e PARAMS" 指定,参数格式: -e PARAMS="--key=value --key2=value2"; * (配置项参考文件:/xxl-job/xxl-job-admin/src/main/resources/application.properties) * 如需自定义 “JVM内存参数”,可通过 "-e JAVA_OPTS" 指定,参数格式: -e JAVA_OPTS="-Xmx512m" * 如需自定义 “日志文件目录”,可通过 "-e LOG_HOME" 指定,参数格式: -e LOG_HOME=/data/applogs */ docker run -d \ -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 \ 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.admin.accessToken=default_token ### 调度中心通讯超时时间[选填],单位秒;默认3s; xxl.job.admin.timeout=3 ### 执行器启用开关 [选填]:默认开启,关闭时不进行执行器初始化; xxl.job.executor.enabled=true ### 执行器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 ### 任务扫描排除路径 [选填] :任务扫描时忽略指定包路径下的Bean;支持配置包路径前缀,多个逗号分隔; xxl.job.executor.excludedpackage=org.springframework,spring ``` #### 步骤三:执行器组件配置 执行器组件,配置文件地址: /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 IDE” 按钮,进入 “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类“@XxlJob”注解自定义的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"注解即可,更加方便、快速。 - 支持自动扫描任务并注入到执行器容器。 - 缺点:略。 >基于方法开发的任务,底层会生成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.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,可以直接配置使用,如下: **通用执行器说明:** - AppName:xxl-job-executor-sample - 执行器代码: - xxl-job-executor-sample-springboot:springboot版本 - xxl-job-executor-sample-frameless:无框架版本 **执行器内置任务列表:** - a、demoJobHandler:简单示例任务,任务内部模拟耗时任务逻辑,用户可在线体验Rolling Log等功能; - b、shardingJobHandler:分片示例任务,任务内部模拟处理分片参数,可参考熟悉分片任务; - c、httpJobHandler:通用HTTP任务Handler;业务方只需要提供HTTP链接等信息即可,不限制语言、平台。任务入参示例如下: ``` // 1、简单示例: { "url": "http://www.baidu.com", "method": "GET", "data": "hello world" } // 2、完整参数示例: { "url": "http://www.baidu.com", // 请求URL "method": "POST", // 请求方法,支持:GET、POST、HEAD、OPTIONS、PUT、DELETE、TRACE "contentType": "application/json", // 请求内容类型,支持:application/json、application/x-www-form-urlencoded、application/xml、text/html、text/xml、text/plain "headers": { // 请求Header,key-value结构 "header01": "value01" }, "cookies": { // 请求Cookie,key-value结构 "cookie01": "value01" }, "timeout": 3000, // 请求超时时间,默认 3000;单位:毫秒; "data": "request body data", // 请求Body数据,仅针对 POST 请求有效 "form": { // 请求Form数据,仅针对 GET 请求有效 "key01": "value01" }, "auth": "auth data" // 请求认证信息, 通过Basic Auth方式认证 } ``` - d、commandJobHandler:通用命令行任务Handler;业务方只需要提供命令行即可,命令及参数之间通过空格隔开;如任务参数 "ls la" 或 "pwd" 将会执行命令并输出数据; #### 原生内置Bean模式任务(AI执行器) 为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式 AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务,如下: **AI执行器说明:** - AppName:xxl-job-executor-sample-ai - 执行器代码:xxl-job-executor-sample-springboot-ai **执行器内置任务列表:** - a、ollamaJobHandler: OllamaChat任务,支持自定义prompt、input等输入信息。示例任务入参如下: ``` { "input": "{输入信息,必填信息}", "prompt": "{模型prompt,可选信息}", "model": "{模型实现,如qwen3:0.6b,可选信息}" } ``` - b、difyWorkflowJobHandler:DifyWorkflow 任务,支持自定义inputs、user、baseUrl、apiKey 等输入信息,示例参数如下; ``` { "inputs":{ // inputs 为dify工作流任务参数;参数不固定,结合各自 workflow 自行定义。 "input":"{用户输入信息}" // 该参数为示例变量,需要 workflow 的“开始”节点 自定义参数 “input”,可自行调整或删除。 }, "user": "xxl-job", // 用户标识,选填 "baseUrl": "http://localhost/v1", // Dify应用的 访问API 地址,需要从 Dify 系统获取; "apiKey": "xxx" // Dify应用的 API-Key,需要从 Dify 系统获取; } ``` - 依赖1:参考 [Ollama本地化部署大模型](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8Ollama%E6%9C%AC%E5%9C%B0%E5%8C%96%E9%83%A8%E7%BD%B2DeepSeek.md) ,执行器示例部署“qwen2.5:1.5b”模型,也可自定选择其他模型版本。 - 依赖2:参考 [使用DeepSeek与Dify搭建AI助手](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8DeepSeek%E4%B8%8EDify%E6%90%AD%E5%BB%BAAI%E5%8A%A9%E6%89%8B.md),执行器示例新建Dify DifyWork应用,并在开始节点添加“input”参数,可结合实际情况调整。 - 依赖3:启动示例 “AI执行器” 相关配置文件说明如下: ``` // ollama 配置 spring.ai.ollama.base-url=http://localhost:11434 spring.ai.ollama.chat.enabled=true // Model模型配置;注意,此处配置模型版本、必须本地先通过ollama进行安装运行。 spring.ai.ollama.chat.options.model=qwen2.5:1.5b spring.ai.ollama.chat.options.temperature=0.8 // dify 配置;选择相关 workflow 应用,切换 “访问API” 页面获取 url 地址信息. dify.base-url=http://localhost/v1 // dify api-key;选择相关 workflow 应用并进入 “访问API” 页面,右上角 “API 密钥” 入口获取 api-key。 dify.api-key={自行获取并修改} ``` ### 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限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性; 排序: 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表; 注册方式:调度中心获取执行器地址的方式; 自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址; 手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用; 机器地址:"注册方式"为"手动录入"时有效,支持人工维护执行器的地址信息;注册地址格式可参考“http://127.0.0.1:9999/”,为执行器内嵌服务地址; ### 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)。 在项目启动时,执行器会通过“@XxlJob”识别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 == 200" 时表示任务执行成功,否则表示任务执行失败,而且可以通过 "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、OpenApi(RESTful 格式):调度中心与执行器提供语言无关的 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镜像构建 可以通过以下命令快速构建调度中心,并启动运行; ``` /** * build package */ mvn clean package /** * build docker image */ docker build -t xuxueli/xxl-job-admin:{指定版本} ./xxl-job-admin /** * 如需自定义 “项目配置文件” 中配置项,比如 mysql 配置,可通过 "-e PARAMS" 指定,参数格式: -e PARAMS="--key=value --key2=value2"; * (配置项参考文件:/xxl-job/xxl-job-admin/src/main/resources/application.properties) * 如需自定义 “JVM内存参数”,可通过 "-e JAVA_OPTS" 指定,参数格式: -e JAVA_OPTS="-Xmx512m" * 如需自定义 “日志文件目录”,可通过 "-e LOG_HOME" 指定,参数格式: -e LOG_HOME=/data/applogs */ docker run -d \ -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 \ xuxueli/xxl-job-admin:{指定版本} ``` ### 5.20 避免任务重复执行 调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发; 针对上述情况,可以通过结合 "单机路由策略(如:第一台、一致性哈希)" + "阻塞策略(如:单机串行、丢弃后续调度)" 来规避,最终避免任务重复执行。 ### 5.21 命令行任务 原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可,命令及参数之间通过空格隔开; 如任务参数 "ls la" 或 "pwd" 将会执行命令并输出数据; ### 5.22 日志自动清理 XXL-JOB日志主要包含如下两部分,均支持日志自动清理,说明如下: - 调度中心日志表数据:可借助配置项 "xxl.job.logretentiondays" 设置日志表数据保存天数,过期日志自动清理;详情可查看上文配置说明; - 执行器日志文件数据:可借助配置项 "xxl.job.executor.logretentiondays" 设置日志文件数据保存天数,过期日志自动清理;详情可查看上文配置说明; ### 5.23 调度结果丢失处理 执行器因网络抖动回调失败或宕机等异常情况,会导致任务调度结果丢失。由于调度中心依赖执行器回调来感知调度结果,因此会导致调度日志永远处于 "运行中" 状态。 针对该问题,调度中心提供内置组件进行处理,逻辑为:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; ### 5.24 Docker Compose 快速部署 支持通过 Docker Compose 方式部署并启动 XXL-JOB,包括:数据库、调度中心、示例执行器。 - 第一步:克隆 XXL-JOB ``` git clone --branch "$(curl -s https://api.github.com/repos/xuxueli/xxl-job/releases/latest | jq -r .tag_name)" https://github.com/xuxueli/xxl-job.git ``` - 第二步:构建 XXL-JOB ``` // 注意:如下命令需要在项目仓库根目录执行 mvn clean package -Dmaven.test.skip=true ``` - 第三步:配置 XXL-JOB ``` // 注意:前往docker目录,自定义 .env 配置;如修改 MYSQL_PATH 配置设置Mysql数据持久化目录; cd ./docker cat .env ``` - 第四步:启动 XXL-JOB ``` // 启动 docker compose up -d // 停止 docker compose down ``` ### 5.25 优雅停机 针对任务调度场景,优雅停机包括调度中心与执行器两部分: - 调度中心优雅停机:调度中心停机时,如果检测到时间轮非空则主动等待等待一段时间,等待调度处理完成;否则立即停机; - 执行器优雅停机:执行器停机时,如果检测到存在运行中任务,在停止接收新任务后会主动等待一段时间,等待任务执行完成;否则立即停机; ## 六、调度中心/执行器 OpenApi XXL-JOB 目标是一种跨平台、跨语言的任务调度规范和协议。 针对Java应用,可以直接通过官方提供的调度中心与执行器,方便快速的接入和使用调度中心,可以参考上文 “快速入门” 章节。 针对非Java应用,可借助 XXL-JOB 的标准 OpenApi(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.openapi.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.openapi.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.constant.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中央仓库, 中央仓库地址以及依赖如下: ``` com.xuxueli xxl-job-core ${最新稳定版} ``` - 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 execute(String param) { XxlJobLogger.log("hello world"); return ReturnT.ofSuccess(); } ``` - 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[2023-03-23] - 1、【优化】执行器任务Bean扫描逻辑优化:解决懒加载注解失效问题; - 2、【优化】多个项目依赖升级至较新稳定版本,涉及netty、groovy、spring、springboot、mybatis等; - 3、【修复】漏洞修复,包括:"CVE-2022-36157" 授权漏洞修复;"CVE-2022-43183" SSRF漏洞修复; ### 7.34 版本 v2.4.1 Release Notes[2024-04-17] - 1、【优化】多个项目依赖升级至较新稳定版本,涉及netty、groovy、springboot、mybatis等; - 2、【优化】执行器注册节点显示交互调整,优化注册节点过多时展示不全体验; - 3、【修复】漏洞修复,包括:"CVE-2022-43402" groovy低版本漏洞修复;"CVE-2024-29025" netty低版本漏洞修复;"CVE-2024-3366" freemarker模板注入漏洞修复;"CVE-2022-43183" 越权漏洞增强修复; - 4、【修复】调度日志页面XSS问题修复(ISSUE-3360)。 ### 7.35 版本 v2.4.2 Release Notes[2024-11-16] - 1、【优化】调度中心任务Next计算逻辑调整,避免Cron解析失败导致重复执行问题。 - 2、【优化】Cron解析组件代码重构微调,健壮性提升; - 3、【优化】修改密码交互调整,避免CSRF隐患; - 4、【优化】JdkSerializeTool流关闭逻辑优化; - 5、【优化】任务信息、执行日志API非功能设计完善,避免越权隐患; - 6、【修复】漏洞修复,包括 "CVE-2024-42681" 子任务越权漏洞修复、"CVE-2023-33779" 任务API越权问题修复; - 7、【升级】多个项目依赖升级至较新稳定版本,涉及netty、groovy、gson、springboot、mybatis等; ### 7.36 版本 v2.5.0 Release Notes[2025-01-11] - 1、【优化】框架基础守护线程异常处理逻辑优化,避免极端情况下因Error导致调度终止问题; - 2、【优化】底层通讯超时时间支持自定义,默认3秒,缓解网络抖动导致任务通讯超时问题;可参考 xxl-job-admin 和 samples 示例代码自行配置; - 3、【修复】调度中心快慢线程池优化拒绝策略,避免因默认AbortPolicy导致调度结果丢失问题; - 4、【优化】调度中心快慢线程池队列长度调整,优化激增调度时任务积压问题; - 5、【重构】调度线程任务信息更新逻辑优化,避免极端情况下已关闭任务被启动问题; - 6、【重构】执行器注册逻辑重构,降低多调度中心地址时并发注册问题;任务注册表新增唯一索引,避免冗余注册信息存储; - 7、【优化】部分系统日志优化,提升可读性; - 8、【优化】合并PR-3616,代码结构注释优化; - 9、【优化】合并PR-3619,避免调度过程中任务停止边界情况处理逻辑; - 10、【优化】合并PR-3605,避免子任务是任务本身导致死循环; - 11、【修复】合并PR-3585,修复全局密码长度不一致问题; - 12、【优化】合并PR-3518,SQL列别名反引号包裹,提升跨数据迁移兼容性; - 13、【优化】合并PR-3518、PR-3400,日志表索引优化,提升大日志量情况下日志查询及清理速度; - 14、【升级】多个项目依赖升级至较新稳定版本,涉及netty、slf4j、junit等; **备注:** - a、本次升级数据模型及通讯协议向前兼容,v2.4.*代码和系统可无缝升级(该版本优化了“xxl_job_log”表索引,建议低版本参考调整); - b、版本v2.5.x为基于jdk8的最后的大版本,将会长期持续维护,问题及漏洞将会及时跟进修复。 - c、下个大版本(v3.0)将会基于 jdk17 与 springboot3.x 构建; ### 7.37 版本 v3.0.0 Release Notes[2025-02-07] - 1、【升级】调度中心升级至 SpringBoot3 + JDK17; - 2、【升级】Docker镜像升级,镜像构建基于JDK17; - 3、【优化】IP获取逻辑优化,优先遍历网卡来获取可用IP; - 4、【优化】通用命令行任务(“commandJobHandler”)优化,支持多参数执行,命令及参数之间通过空格隔开;如任务参数 "ls la" 或 "pwd" 将会执行命令并输出数据; - 5、【优化】通用HTTP任务(httpJobHandler)优化,任务参数格式调整为json格式; - 6、【升级】多个项目依赖升级至较新稳定版本,涉及 netty、groovy、spring/springboot 等; **备注:** - a、本次升级数据模型及通讯协议向前兼容,v2.4.*及后续版本可无缝升级; - b、版本3.x开始要求Jdk17;版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求,可选择接入不同版本; ### 7.38 版本 v3.1.0 Release Notes[2025-05-01] - 1、【新增】新增提供 “AI执行器” 并内置多个Bean模式 AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。 - AppName:xxl-job-executor-sample-ai - 执行器代码:xxl-job-executor-sample-springboot-ai - 执行器初始化脚本:执行参考SQL脚本,或自行人工创建: ``` INSERT INTO `xxl_job_group`(`app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES ('xxl-job-executor-sample-ai', 'AI执行器Sample', 0, NULL, now()); ``` - 2、【新增】新增多个 Bean模式 AI任务Handler,如 ollamaJobHandler、difyWorkflowJobHandler 等,支持快速集成开发AI任务。任务配置可参考 [AI执行器](https://www.xuxueli.com/xxl-job/#原生内置Bean模式任务(AI执行器)) - a、ollamaJobHandler: OllamaChat任务,支持自定义prompt、input等输入信息。 - b、difyWorkflowJobHandler:DifyWorkflow 任务,支持自定义inputs、user、baseUrl、apiKey等输入信息。 - 3、【修复】合并PR-3708、PR-3704,解决固定速度调度模式下,下次计算执行时间小概率(间隔超长时)不准问题。 - 4、【修复】任务操作逻辑优化,修复边界情况下逻辑中断问题 (ISSUE-2081)。 - 5、【修复】调度中心Cron前端组件优化,解决week配置与后端兼容性问题 (ISSUE-2220)。 - 6、【修复】任务RollingLog权限逻辑调整:修复非管理员账号越权访问问题 (ISSUE-3705)。 - 7、【优化】Glue IDE调整,版本回溯支持查看修改时间; - 8、【优化】任务RollingLog调整,XSS过滤支持白名单排出,提升日志易读性; - 9、【优化】执行器日志文件保存天数(logretentiondays)调整,最小保留时间调整至3天。 - 10、【升级】多个项目依赖升级至较新稳定版本,涉及 gson、groovy、spring/springboot、mysql 等; ### 7.39 版本 v3.1.1 Release Notes[2025-06-23] - 1、【调整】AI任务(difyWorkflowJobHandler)优化:针对 “baseUrl、apiKey” 等Dify配置信息,从执行器侧文件类配置调整至调度中心“任务参数”动态配置,支持多Dify应用集成并提升研发效率; - 2、【优化】合并PR-2417,修复任务管理时JobHandler录入空格问题; - 3、【优化】合并PR-2504,规避SQL注入问题; - 4、【升级】多个项目依赖升级至较新稳定版本,涉及 netty、spring/springboot、groovy 等; ### 7.40 版本 v3.2.0 Release Notes[2025-08-24] - 1、【强化】AI任务(ollamaJobHandler)优化:针对 “model” 模型配置信息,从执行器侧文件类配置调整至调度中心“任务参数”动态配置,支持集成多模型、并结合任务动态配置切换。 - 2、【安全】登录认证重构:密码加密算法从Md5改为Sha256;登录态改为登录后动态随机生成;提升系统安全性;(需要针对用户表进行字段调整,同时需要重新初始化密码信息;相关SQL脚本如下) ``` // 1、用户表password字段需要调整长度,执行如下命令 ALTER TABLE xxl_job_user MODIFY COLUMN `password` varchar(100) NOT NULL COMMENT '密码加密信息'; ALTER TABLE xxl_job_user ADD COLUMN `token` varchar(100) DEFAULT NULL COMMENT '登录token'; // 2、存量用户密码需要修改,可执行如下命令将密码初始化 “123456”;也可以自行通过 “SHA256Tool.sha256” 工具生成其他初始化密码; UPDATE xxl_job_user t SET t.password = '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92' WHERE t.username = {用户名}; ``` - 3、【强化】GLUE模式(Python) 扩展,支持 "GLUE(Python3)" 与 "GLUE(Python2)" 两种模式,分别支持 python3/2 多版本; - 4、【强化】调度中心系统日志调整,支持启动时指定 -DLOG_HOME 参数自定义日志位置;同时优化日志格式提升易读性; - 5、【优化】任务Bean扫描规则调整,过滤冗余不必要扫描,避免系统组件提前初始化; - 6、【优化】登录信息页面空值处理优化,避免空值影响ftl渲染; - 7、【优化】异常页面处理逻辑优化,新增兜底落地页配置; - 8、【重构】ReturnT 重构,简化代码结构,提升API易用性以及可维护性; - 9、【重构】项目结构重构,提升可维护性与易读性; - 10、【修复】漏洞修复(CVE-2025-7787),针对 httpJobHandler 支持配置URL白名单限制,防止服务器端请求伪造(SSRF)攻击。 - 11、【修复】合并PR-3738,修复拼写问题; - 12、【修复】合并PR-3506,修复小概率情况下任务重复调度问题; - 13、【修复】合并PR-3747,修复异常情况下资源泄漏风险; - 14、【修复】IDOR越权问题修复,提升任务操作及日志管理安全性; - 15、【升级】升级多项maven依赖至较新版本,如 netty、groovy、mybatis、spring、spring-ai、dify 等; ### 7.41 版本 v3.3.0 Release Notes[2025-11-29] - 1、【新增】执行器新增“任务扫描排除路径”配置项(xxl.job.executor.excludedpackage),任务扫描时忽略指定包路径下的任务; - 2、【优化】执行器任务Bean扫描逻辑调整,优化懒加载Bean检测及过滤机制,避免提前初始化类问题; - 3、【新增】合并PR-3840,执行器支持通过XxlJobHelper获取任务触发时间戳;XxlJobHelper组件完善,支持通过“XxlJobHelper.getLogId/getLogDateTime/getLogFileName”方法获取执行日志相关信息; - 4、【升级】调度中心UI框架升级,统一交互组件,支持多主题、多标签与局部渲染等,升级UI组件及性能; - 5、【优化】调度时间轮组件强化,保障不重不漏:调度时间轮单刻度数据去重,避免极端情况下任务重复执行;时间轮转动时校验临近刻度,避免极端情况下遗漏刻度; - 6、【优化】调度任务锁逻辑优化,事务SQL下沉至Mapper层统一管理,并增加测试用例,提升代码可读性以及可维护性; - 7、【优化】调度快慢线程池默认配置上调,提升默认配置单机负载;调度预读任务数计算系数下调,降低事务颗粒度,提升性能及稳定性; - 8、【性能】调度中心调整资源加载逻辑,移除不必要的拦截器,提升页面加载性能; - 9、【优化】优化日志列表页面展示逻辑,新增展示“日志ID”与“任务名称”信息; - 10、【优化】报表统计SQL优化,修复小概率情况下查询null值问题;报表初始化SQL优化,修复小概率情况增改竞争问题; - 11、【优化】优日志报告与清理逻辑,增加清理过期日志的异常捕获,避免线程异常退出; - 12、【优化】任务回调失败日志读写磁盘逻辑优化,解决极端情况下大文件读写内存问题; - 13、【升级】Http通讯组件升级,基于接口代理方式重构通讯组件,提升组件性能及扩展性; - 14、【重构】规范API交互协议,通用响应结构体调整为Response,调度中心API统一为Response封装数据; (注意:响应结构体从ReturnT升级为Response,其中属性值“content”会调整为“data”,通过openapi交互场景需要关注) - 15、【重构】调度过期策略、调度类型策略逻辑重构,代码组件化拆分并完善日志,提升健壮性及可维护性; - 16、【重构】调度中心底层组件重构,组件初始化以及销毁逻辑统一处理,任务触发及和回调逻辑优化,避免资源泄漏风险; - 17、【重构】调度中心底层组件模块化拆分,移除组件单例以及静态代码逻辑,提升组件可维护性; - 18、【重构】重构Rolling日志读写逻辑,解决边界条件下异常情况,优化读写性能; - 19、【修复】脚本任务process销毁逻辑优化,解决风险情况下脚本进程无法终止问题; - 20、【修复】合并PR-2369,修复脚本任务参数取值问题; - 21、【新增】任务审计日志,记录任务操作敏感日志信息,如任务新建/更新/删除/启停/触发以及GLUE代码更新等,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题等。 (当前任务审计日志以Info级别输出在系统日志中,可通过关键词 "xxl-job operation log:" 检索过滤) - 22、【强化】通用HTTP任务(httpJobHandler)强化,支持更丰富请求参数设置,完整参数示例如下:
完整参数示例参考: ``` { "url": "http://www.baidu.com", "method": "POST", "contentType": "application/json", "headers": { "header01": "value01" }, "cookies": { "cookie01": "value01" }, "timeout": 3000, "data": "request body data", "form": { "key01": "value01" }, "auth": "auth data" } ```
- 23、【优化】调度组件日志完善,提升边界情况下问题定位效率; - 24、【升级】升级多项maven依赖至较新版本,如 netty、groovy、springboot、spring-ai、dify、mybatis、xxl-sso 等; **备注:** - a、本次升级数据模型向前兼容,v3.2.*版本可直接升级不需要进行数据库表调整; - b、本次升级针对客户端rollinglog依赖字段做规范约束,如不关注该功能 v2.4.* 及后续版本客户端不需要升级/可兼容,否则需要升级客户端版本; ### 7.42 版本 v3.3.1 Release Notes[2025-12-06] - 1、【新增】新增“执行器启用开关”配置项(xxl.job.executor.enabled),默认开启,关闭时不进行执行器初始化; - 2、【修复】调度组件事务代码调整,修复DB超时等小概率情况下调度终止问题; - 3、【修复】合并PR-3869,修复底层通讯超时设置无效问题; - 4、【优化】执行器删除逻辑优化,删除时一并清理注册表数据,避免小概率情况下注册数据堆积(ISSUE-3669); - 5、【升级】调度中心升级至 SpringBoot4;升级多项maven依赖至较新版本,如 mybatis、groovy 等; ### 7.43 版本 v3.3.2 Release Notes[2026-01-01] - 1、【优化】优雅停机:调度中心停机,检测时间轮非空时主动等待调度完成;客户端停机,检测存在运行中任务时,停止接收新任务并主动等待任务执行完成; - 2、【新增】新增 Docker Compose 配置,支持一键配置启动调度中心集群;
Docker Compose启动步骤: ``` // 下载 XXL-JOB git clone --branch "$(curl -s https://api.github.com/repos/xuxueli/xxl-job/releases/latest | jq -r .tag_name)" https://github.com/xuxueli/xxl-job.git // 构建 XXL-JOB mvn clean package -Dmaven.test.skip=true // 配置 XXL-JOB(前往docker目录,自定义 .env) cd ./docker cat .env // 启动 XXL-JOB docker compose up -d // 停止 XXL-JOB docker compose down ```
- 3、【优化】调度中心操作体验优化:表格交互调整为单行选中模式;禁用分页循环;优化分页限制文案; - 4、【优化】调度线程事务提交逻辑调整,避免边界条件下线程异常退出,增强健壮性; - 5、【优化】调度日志列表排序逻辑优化,提升易读性; - 6、【优化】调度中心OpenAPI通讯token调整为非必填;合并PR-3892; - 7、【优化】执行器详情接口权限调整,支持普通用户查看注册节点;合并PR-3882; - 8、【优化】任务参数LogDateTime生成逻辑调整,分片广播场景下保障同一批调度一致; - 9、【升级】升级多项maven依赖至较新版本,如 spring、netty、xxl-sso、xxl-tool 等; - 10、【优化】统一项目依赖管理结构,依赖版本统一到父级pom提升可维护性; ### 7.44 版本 v3.4.0 Release Notes[ING] - 1、【TODO】调度触发性能优化:调度触发后任务分批批量更新,提升调度性能; - 2、【TODO】执行器内嵌容器调整:由Netty调整为Tomcat,简化项目依赖; ### TODO LIST - 1、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。 - 2、任务优先级:调度与执行阶段按照优先级分配资源。 - 3、多数据库支持,DAO层通过JPA实现,不限制数据库类型。 - 4、OpenApi: - 执行器Log文件清理:支持调度中心远程删除执行器中指定任务的Log文件; - 5、性能优化:任务、执行器数据全量本地缓存;新增消息表广播通知; - 6、DAG流程任务 - 子任务:废弃 - DAG任务创建、管理,DAG任务日志查看、操作; - 支持参数传递,共享数据; - 分片任务:全部完成后才会出发后置节点; - 配置并列的"a-b、b-c"路径列表,构成串行、并行、dag任务流程,"dagre-d3"绘图;任务依赖,流程图,子任务+会签任务,各节点日志;支持根据成功、失败选择分支; - 7、任务标签:方便搜索; - 8、GLUE 模式 Web Ide 版本对比功能; - 9、自定义失败重试时间间隔; - 10、任务导入导出工具,灵活支持版本升级、迁移等场景。 - 11、任务日志重构:一次调度只记录一条主任务,维护起止时间和状态。 - 普通任务:只记录一条主任务; - 广播任务:记录一条主任务,每个分片任务记录一条次任务,关联在主任务上; - 重试任务:失败时,新增主任务。所有调度记录,包括入口调度和重试调度,均挂载主任务上。 - 12、分片任务:全部完成后才会出发后置节点; - 13、日期过滤:支持多个时间段排除; - 14、提供执行器Docker镜像; - 15、脚本任务,支持数据参数,新版本仅支持单参数不支持需要兼容; - 17、批量调度:调度请求入queue,调度线程批量获取调度请求并发起远程调度;提高线程效率; - 18、执行器端口复用,复用容器端口提供通讯服务; - 19、安全功能增强,通讯加密参数改用加密数据避免AccessToken明文, 降低token泄漏风险; - 20、告警增强: - 邮件告警:支持自定义标题、模板格式; - webhook告警:支持自定义告警URL、请求体格式; - 21、公共告警策略:执行器维度设置多告警策略,任务勾选启用;待评估任务或执行器维度; - 20、日志策略: - 调度日志:全局配置:废弃; 新增“调度日志策略”:任务维度自定义,保留3天、7天、1个月、3个月、一年、永久; - 执行日志:新增“执行RollingLog开关”:任务维度自定义,支持:RollingLog、普通日志(slf4j输出)、关闭(不输出); - 21、AccessToken:废弃全局配置;支持在线管理,动态生成、动态启停; - 22、任务执行后分批批量更新,提升调度性能; - 23、任务管理OpenAPI; - 24、调度中心启动参数线上配置:告警发送邮箱、Token,支持线上配置生效,修改不需重启机器; - 25、执行器内嵌server切换tomcat,精简依赖; - 26、日志策略新增: - 调度日志策略:任务级设置,最少保留1天。 - 执行日志策略:可选 RollingLog、slf4jLog; - 清理逻辑,性能重构。 ## 八、其他 ### 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 ) ================================================ FILE: doc/db/tables_xxl_job.sql ================================================ # # XXL-JOB # 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; ## —————————————————————— job group and registry —————————————————— 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_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`), UNIQUE KEY `i_g_k_v` (`registry_group`, `registry_key`, `registry_value`) USING BTREE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; ## —————————————————————— job info —————————————————— 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_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; ## —————————————————————— job log and report —————————————————— 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`), KEY `I_jobid_jobgroup` (`job_id`,`job_group`), KEY `I_job_id` (`job_id`) ) 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; ## —————————————————————— lock —————————————————— CREATE TABLE `xxl_job_lock` ( `lock_name` varchar(50) NOT NULL COMMENT '锁名称', PRIMARY KEY (`lock_name`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; ## —————————————————————— user —————————————————— CREATE TABLE `xxl_job_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '账号', `password` varchar(100) NOT NULL COMMENT '密码加密信息', `token` varchar(100) DEFAULT NULL COMMENT '登录token', `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; ## —————————————————————— for default data —————————————————— INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '通用执行器Sample', 0, NULL, now()), (2, 'xxl-job-executor-sample-ai', 'AI执行器Sample', 0, NULL, now()); 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, '示例任务01', now(), now(), 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', now(), ''), (2, 2, 'Ollama示例任务01', now(), now(), 'XXL', '', 'NONE', '', 'DO_NOTHING', 'FIRST', 'ollamaJobHandler', '{ "input": "慢SQL问题分析思路", "prompt": "你是一个研发工程师,擅长解决技术类问题。", "model": "qwen3:0.6b" }', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', now(), ''), (3, 2, 'Dify示例任务', now(), now(), 'XXL', '', 'NONE', '', 'DO_NOTHING', 'FIRST', 'difyWorkflowJobHandler', '{ "inputs":{ "input":"查询班级各学科前三名" }, "user": "xxl-job", "baseUrl": "http://localhost/v1", "apiKey": "app-OUVgNUOQRIMokfmuJvBJoUTN" }', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', now(), ''); INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 1, NULL); INSERT INTO `xxl_job_lock` (`lock_name`) VALUES ('schedule_lock'); commit; ================================================ FILE: docker/docker-compose.yml ================================================ # docker-compose version version: '3.8' services: mysql: image: mysql:8.4 container_name: xxl-job-mysql environment: # 1、数据库密码设置,需要与Admin中配置一致: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} # 2、数据库实例名称,需要与Admin中配置一致; MYSQL_DATABASE: xxl_job ports: - "3306:3306" volumes: # 说明:仅数据库首次初始化时执行; - ../doc/db/tables_xxl_job.sql:/docker-entrypoint-initdb.d/tables_xxl_job.sql:ro # 3、数据库持久化目录位置,建议自定义: - ${MYSQL_PATH}/conf:/etc/mysql/conf.d - ${MYSQL_PATH}/logs:/var/log/mysql - ${MYSQL_PATH}/data:/var/lib/mysql command: >- --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 networks: - xxl-job-network xxl-job-admin: # 4、本地Build设置,如果期望使用推动DockerHub的镜像,可以注释当前启用的image、build配置,并启用如下设置版本的image配置; #image: xuxueli/xxl-job-admin:{version} image: xuxueli/xxl-job-admin:local build: context: ../xxl-job-admin dockerfile: Dockerfile container_name: xxl-job-admin environment: # 5、数据库密码设置,需要与上文Mysql中保持一致: PARAMS: >- --spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=${MYSQL_ROOT_PASSWORD} --server.port=${XXL_JOB_ADMIN_PORT} --server.servlet.context-path=${XXL_JOB_ADMIN_CONTEXT_PATH} ports: - "${XXL_JOB_ADMIN_PORT}:${XXL_JOB_ADMIN_PORT}" depends_on: mysql: condition: service_healthy networks: - xxl-job-network xxl-job-executor-sample-springboot: image: xuxueli/xxl-job-executor-sample-springboot:local build: context: ./xxl-job-executor-samples/xxl-job-executor-sample-springboot dockerfile: Dockerfile container_name: xxl-job-executor-sample-springboot environment: PARAMS: >- --xxl.job.admin.addresses=http://xxl-job-admin:${XXL_JOB_ADMIN_PORT}${XXL_JOB_ADMIN_CONTEXT_PATH:-/} ports: - "9999:9999" depends_on: xxl-job-admin: condition: service_started networks: - xxl-job-network networks: xxl-job-network: driver: bridge ================================================ FILE: pom.xml ================================================ 4.0.0 com.xuxueli xxl-job 3.4.0-SNAPSHOT pom ${project.artifactId} A distributed task scheduling framework. https://www.xuxueli.com/ xxl-job-core xxl-job-admin xxl-job-executor-samples UTF-8 UTF-8 UTF-8 17 17 true 3.14.1 3.4.0 3.12.0 3.2.8 0.9.0 2.0.17 6.0.1 3.0.0 4.0.1 7.0.2 4.0.1 9.5.0 4.2.9.Final 5.0.3 2.3.2 2.4.2 2.13.2 2.0.0-M1 1.2.3 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.slf4j slf4j-api ${slf4j-api.version} org.slf4j slf4j-reload4j ${slf4j-api.version} org.junit.jupiter junit-jupiter-engine ${junit-jupiter.version} jakarta.annotation jakarta.annotation-api ${jakarta.annotation-api.version} io.netty netty-codec-http ${netty.version} com.google.code.gson gson ${gson.version} com.xuxueli xxl-tool ${xxl-tool.version} org.apache.groovy groovy ${groovy.version} org.springframework spring-context ${spring.version} provided org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis-spring-boot-starter.version} com.mysql mysql-connector-j ${mysql-connector-j.version} com.xuxueli xxl-job-core ${project.parent.version} com.xuxueli xxl-sso-core ${xxl-sso.version} org.springframework.ai spring-ai-starter-model-ollama ${spring-ai.version} io.github.imfangs dify-java-client ${dify-java-client.version} GNU General Public License version 3 https://opensource.org/licenses/GPL-3.0 master https://github.com/xuxueli/xxl-job.git scm:git:https://github.com/xuxueli/xxl-job.git scm:git:git@github.com:xuxueli/xxl-job.git XXL xuxueli 931591021@qq.com https://github.com/xuxueli release true xxl-job-core org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} ${maven.compiler.source} ${maven.compiler.target} true org.apache.maven.plugins maven-source-plugin ${maven-source-plugin.version} package jar-no-fork org.apache.maven.plugins maven-javadoc-plugin ${maven-javadoc-plugin.version} package jar none org.apache.maven.plugins maven-gpg-plugin ${maven-gpg-plugin.version} false verify sign org.sonatype.central central-publishing-maven-plugin ${central-publishing-maven-plugin.version} true central xxl-job-admin xxl-job-executor-samples xxl-job-executor-sample-frameless xxl-job-executor-sample-springboot xxl-job-executor-sample-springboot-ai ================================================ FILE: xxl-job-admin/Dockerfile ================================================ # base image FROM openjdk:21-jdk-slim # maintainer MAINTAINER xuxueli # set params ENV PARAMS="" # set timezone ENV TZ=PRC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # copy jar ADD target/xxl-job-admin-*.jar /app.jar # command # log home: -e LOG_HOME=/data/applogs # jvm options: -e JAVA_OPTS="-Xms128m -Xmx128m" # app params: -e PARAMS="--server.port=8080" ENTRYPOINT ["sh","-c","java ${LOG_HOME:+-DLOG_HOME=$LOG_HOME} -jar $JAVA_OPTS /app.jar $PARAMS"] ================================================ FILE: xxl-job-admin/pom.xml ================================================ 4.0.0 com.xuxueli xxl-job 3.4.0-SNAPSHOT xxl-job-admin jar true org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-freemarker org.springframework.boot spring-boot-starter-mail org.springframework.boot spring-boot-starter-actuator org.mybatis.spring.boot mybatis-spring-boot-starter com.mysql mysql-connector-j com.xuxueli xxl-job-core com.xuxueli xxl-sso-core org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} repackage ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java ================================================ 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); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/constant/Consts.java ================================================ package com.xxl.job.admin.constant; public class Consts { public static final String ADMIN_ROLE = "ADMIN"; } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/constant/TriggerStatus.java ================================================ package com.xxl.job.admin.constant; public enum TriggerStatus { STOPPED(0, "stopped"), RUNNING(1, "running"); private int value; private String desc; TriggerStatus(int value, String desc) { this.value = value; this.desc = desc; } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/base/IndexController.java ================================================ package com.xxl.job.admin.controller.base; import com.xxl.job.admin.constant.Consts; import com.xxl.job.admin.model.dto.XxlBootResourceDTO; import com.xxl.job.admin.service.XxlJobService; import com.xxl.job.admin.util.I18nUtil; import com.xxl.sso.core.annotation.XxlSso; import com.xxl.sso.core.helper.XxlSsoHelper; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.StringTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; /** * index controller * * @author xuxueli 2015-12-19 16:13:16 */ @Controller public class IndexController { @Resource private XxlJobService xxlJobService; /** * index */ @RequestMapping("/") @XxlSso public String index(HttpServletRequest request, Model model) { // menu resource List resourceList = findResourceList(request); model.addAttribute("resourceList", resourceList); return "base/index"; } /** * fill menu data */ private List findResourceList(HttpServletRequest request){ // login check Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); // init menu-list List resourceDTOList = Arrays.asList( new XxlBootResourceDTO(1, 0, I18nUtil.getString("job_dashboard_name"),1, "", "/dashboard", "fa-home", 1, 0, null), new XxlBootResourceDTO(2, 0, I18nUtil.getString("jobinfo_name"),1, "", "/jobinfo", " fa-clock-o", 2, 0, null), new XxlBootResourceDTO(3, 0, I18nUtil.getString("joblog_name"),1, "", "/joblog", " fa-database", 3, 0, null), new XxlBootResourceDTO(4, 0, I18nUtil.getString("jobgroup_name"),1, Consts.ADMIN_ROLE, "/jobgroup", " fa-cloud", 4, 0,null), new XxlBootResourceDTO(5, 0, I18nUtil.getString("user_manage"),1, Consts.ADMIN_ROLE, "/user", "fa-users", 5, 0, null), new XxlBootResourceDTO(9, 0, I18nUtil.getString("admin_help"),1, "", "/help", "fa-book", 6, 0, null) ); // filter by role if (!XxlSsoHelper.hasRole(loginInfoResponse.getData(), Consts.ADMIN_ROLE).isSuccess()) { resourceDTOList = resourceDTOList.stream() .filter(resourceDTO -> StringTool.isBlank(resourceDTO.getPermission() )) // normal user had no permission .collect(Collectors.toList()); } resourceDTOList.stream().sorted(Comparator.comparing(XxlBootResourceDTO::getOrder)).toList(); return resourceDTOList; } /** * dashboard */ @RequestMapping("/dashboard") @XxlSso public String dashboard(HttpServletRequest request, Model model) { Map dashboardMap = xxlJobService.dashboardInfo(); model.addAllAttributes(dashboardMap); return "base/dashboard"; } @RequestMapping("/chartInfo") @ResponseBody public Response> chartInfo(@RequestParam("startDate") Date startDate, @RequestParam("endDate") Date endDate) { Response> chartInfo = xxlJobService.chartInfo(startDate, endDate); return chartInfo; } /** * help */ @RequestMapping("/help") @XxlSso public String help() { return "base/help"; } @RequestMapping(value = "/errorpage") @XxlSso(login = false) public ModelAndView errorPage(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) { String exceptionMsg = "HTTP Status Code: "+response.getStatus(); mv.addObject("exceptionMsg", exceptionMsg); mv.setViewName("common/common.errorpage"); return mv; } @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)); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/base/LoginController.java ================================================ package com.xxl.job.admin.controller.base; import com.xxl.job.admin.mapper.XxlJobUserMapper; import com.xxl.job.admin.model.XxlJobUser; import com.xxl.job.admin.util.I18nUtil; import com.xxl.sso.core.annotation.XxlSso; import com.xxl.sso.core.helper.XxlSsoHelper; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.StringTool; import com.xxl.tool.crypto.Sha256Tool; import com.xxl.tool.id.UUIDTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; 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; /** * index controller * @author xuxueli 2015-12-19 16:13:16 */ @Controller @RequestMapping("/auth") public class LoginController { @Resource private XxlJobUserMapper xxlJobUserMapper; @RequestMapping("/login") @XxlSso(login = false) public ModelAndView login(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView) { // xxl-sso, logincheck Response loginInfoResponse = XxlSsoHelper.loginCheckWithCookie(request, response); if (loginInfoResponse.isSuccess()) { modelAndView.setView(new RedirectView("/",true,false)); return modelAndView; } return new ModelAndView("base/login"); } @RequestMapping(value="/doLogin", method=RequestMethod.POST) @ResponseBody @XxlSso(login=false) public Response doLogin(HttpServletRequest request, HttpServletResponse response, String userName, String password, String ifRemember){ // param boolean ifRem = StringTool.isNotBlank(ifRemember) && "on".equals(ifRemember); if (StringTool.isBlank(userName) || StringTool.isBlank(password)){ return Response.ofFail( I18nUtil.getString("login_param_empty") ); } // valid user、status XxlJobUser xxlJobUser = xxlJobUserMapper.loadByUserName(userName); if (xxlJobUser == null) { return Response.ofFail( I18nUtil.getString("login_param_unvalid") ); } // valid passowrd String passwordHash = Sha256Tool.sha256(password); if (!passwordHash.equals(xxlJobUser.getPassword())) { return Response.ofFail( I18nUtil.getString("login_param_unvalid") ); } // xxl-sso, do login LoginInfo loginInfo = new LoginInfo(String.valueOf(xxlJobUser.getId()), UUIDTool.getSimpleUUID()); Response result= XxlSsoHelper.loginWithCookie(loginInfo, response, ifRem); return Response.of(result.getCode(), result.getMsg()); } @RequestMapping(value="/logout", method=RequestMethod.POST) @ResponseBody @XxlSso(login=false) public Response logout(HttpServletRequest request, HttpServletResponse response){ // xxl-sso, do logout Response result = XxlSsoHelper.logoutWithCookie(request, response); return Response.of(result.getCode(), result.getMsg()); } @RequestMapping("/updatePwd") @ResponseBody @XxlSso public Response updatePwd(HttpServletRequest request, String oldPassword, String password){ // valid if (oldPassword==null || oldPassword.trim().isEmpty()){ return Response.ofFail(I18nUtil.getString("system_please_input") + I18nUtil.getString("change_pwd_field_oldpwd")); } if (password==null || password.trim().isEmpty()){ return Response.ofFail(I18nUtil.getString("system_please_input") + I18nUtil.getString("change_pwd_field_oldpwd")); } password = password.trim(); if (!(password.length()>=4 && password.length()<=20)) { return Response.ofFail(I18nUtil.getString("system_lengh_limit")+"[4-20]" ); } // md5 password String oldPasswordHash = Sha256Tool.sha256(oldPassword); String passwordHash = Sha256Tool.sha256(password); // valid old pwd Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); XxlJobUser existUser = xxlJobUserMapper.loadByUserName(loginInfoResponse.getData().getUserName()); if (!oldPasswordHash.equals(existUser.getPassword())) { return Response.ofFail(I18nUtil.getString("change_pwd_field_oldpwd") + I18nUtil.getString("system_unvalid")); } // write new existUser.setPassword(passwordHash); xxlJobUserMapper.update(existUser); return Response.ofSuccess(); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobCodeController.java ================================================ package com.xxl.job.admin.controller.biz; import com.xxl.job.admin.mapper.XxlJobInfoMapper; import com.xxl.job.admin.mapper.XxlJobLogGlueMapper; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLogGlue; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.admin.util.JobGroupPermissionUtil; import com.xxl.job.core.glue.GlueTypeEnum; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.StringTool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; 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 java.util.Date; import java.util.List; /** * job code controller * @author xuxueli 2015-12-19 16:13:16 */ @Controller @RequestMapping("/jobcode") public class JobCodeController { private static final Logger logger = LoggerFactory.getLogger(JobCodeController.class); @Resource private XxlJobInfoMapper xxlJobInfoMapper; @Resource private XxlJobLogGlueMapper xxlJobLogGlueMapper; @RequestMapping public String index(HttpServletRequest request, Model model, @RequestParam("jobId") int jobId) { XxlJobInfo jobInfo = xxlJobInfoMapper.loadById(jobId); List jobLogGlues = xxlJobLogGlueMapper.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 jobGroup permission JobGroupPermissionUtil.validJobGroupPermission(request, jobInfo.getJobGroup()); // Glue类型-字典 model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); model.addAttribute("jobInfo", jobInfo); model.addAttribute("jobLogGlues", jobLogGlues); return "biz/job.code"; } @RequestMapping("/save") @ResponseBody public Response save(HttpServletRequest request, @RequestParam("id") int id, @RequestParam("glueSource") String glueSource, @RequestParam("glueRemark") String glueRemark) { // valid if (StringTool.isBlank(glueSource)) { return Response.ofFail( (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_source")) ); } if (glueRemark==null) { return Response.ofFail( (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) ); } if (glueRemark.length()<4 || glueRemark.length()>100) { return Response.ofFail(I18nUtil.getString("jobinfo_glue_remark_limit")); } XxlJobInfo existsJobInfo = xxlJobInfoMapper.loadById(id); if (existsJobInfo == null) { return Response.ofFail( I18nUtil.getString("jobinfo_glue_jobid_unvalid")); } // valid jobGroup permission LoginInfo loginInfo = JobGroupPermissionUtil.validJobGroupPermission(request, existsJobInfo.getJobGroup()); // update new code existsJobInfo.setGlueSource(glueSource); existsJobInfo.setGlueRemark(glueRemark); existsJobInfo.setGlueUpdatetime(new Date()); existsJobInfo.setUpdateTime(new Date()); xxlJobInfoMapper.update(existsJobInfo); // log old code XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue(); xxlJobLogGlue.setJobId(existsJobInfo.getId()); xxlJobLogGlue.setGlueType(existsJobInfo.getGlueType()); xxlJobLogGlue.setGlueSource(glueSource); xxlJobLogGlue.setGlueRemark(glueRemark); xxlJobLogGlue.setAddTime(new Date()); xxlJobLogGlue.setUpdateTime(new Date()); xxlJobLogGlueMapper.save(xxlJobLogGlue); // remove code backup more than 30 xxlJobLogGlueMapper.removeOld(existsJobInfo.getId(), 30); // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobcode-update", GsonTool.toJson(xxlJobLogGlue)); return Response.ofSuccess(); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobGroupController.java ================================================ package com.xxl.job.admin.controller.biz; import com.xxl.job.admin.constant.Consts; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobRegistry; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.admin.mapper.XxlJobGroupMapper; import com.xxl.job.admin.mapper.XxlJobInfoMapper; import com.xxl.job.admin.mapper.XxlJobRegistryMapper; import com.xxl.job.core.constant.Const; import com.xxl.job.core.constant.RegistType; import com.xxl.sso.core.annotation.XxlSso; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.http.HttpTool; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; 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 java.util.*; /** * job group controller * @author xuxueli 2016-10-02 20:52:56 */ @Controller @RequestMapping("/jobgroup") public class JobGroupController { @Resource public XxlJobInfoMapper xxlJobInfoMapper; @Resource public XxlJobGroupMapper xxlJobGroupMapper; @Resource private XxlJobRegistryMapper xxlJobRegistryMapper; @RequestMapping @XxlSso(role = Consts.ADMIN_ROLE) public String index(Model model) { return "biz/group.list"; } @RequestMapping("/pageList") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response> pageList(@RequestParam(required = false, defaultValue = "0") int offset, @RequestParam(required = false, defaultValue = "10") int pagesize, String appname, String title) { // page query List list = xxlJobGroupMapper.pageList(offset, pagesize, appname, title); int list_count = xxlJobGroupMapper.pageListCount(offset, pagesize, appname, title); // package result PageModel pageModel = new PageModel<>(); pageModel.setData(list); pageModel.setTotal(list_count); return Response.ofSuccess(pageModel); } @RequestMapping("/insert") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response insert(XxlJobGroup xxlJobGroup){ // valid if (StringTool.isBlank(xxlJobGroup.getAppname())) { return Response.ofFail((I18nUtil.getString("system_please_input")+"AppName") ); } if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { return Response.ofFail( I18nUtil.getString("jobgroup_field_appname_length") ); } if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) { return Response.ofFail( "AppName"+I18nUtil.getString("system_unvalid") ); } if (StringTool.isBlank(xxlJobGroup.getTitle())) { return Response.ofFail((I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); } if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) { return Response.ofFail(I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") ); } if (xxlJobGroup.getAddressType()!=0) { if (StringTool.isBlank(xxlJobGroup.getAddressList())) { return Response.ofFail( I18nUtil.getString("jobgroup_field_addressType_limit") ); } if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) { return Response.ofFail(I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") ); } String[] addresss = xxlJobGroup.getAddressList().split(","); for (String item: addresss) { if (StringTool.isBlank(item)) { return Response.ofFail( I18nUtil.getString("jobgroup_field_registryList_unvalid") ); } if (!(HttpTool.isHttp(item) || HttpTool.isHttps(item))) { return Response.ofFail( I18nUtil.getString("jobgroup_field_registryList_unvalid")+"[2]" ); } } } // process xxlJobGroup.setUpdateTime(new Date()); int ret = xxlJobGroupMapper.save(xxlJobGroup); return (ret>0)?Response.ofSuccess():Response.ofFail(); } @RequestMapping("/update") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response update(XxlJobGroup xxlJobGroup){ // valid if (StringTool.isBlank(xxlJobGroup.getAppname())) { return Response.ofFail((I18nUtil.getString("system_please_input")+"AppName") ); } if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { return Response.ofFail( I18nUtil.getString("jobgroup_field_appname_length") ); } if (StringTool.isBlank(xxlJobGroup.getTitle())) { return Response.ofFail( (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); } if (xxlJobGroup.getAddressType() == 0) { // 0=自动注册 List registryList = findRegistryByAppName(xxlJobGroup.getAppname()); String addressListStr = null; if (CollectionTool.isNotEmpty(registryList)) { Collections.sort(registryList); addressListStr = String.join(",", registryList); } xxlJobGroup.setAddressList(addressListStr); } else { // 1=手动录入 if (StringTool.isBlank(xxlJobGroup.getAddressList())) { return Response.ofFail( I18nUtil.getString("jobgroup_field_addressType_limit") ); } String[] addresss = xxlJobGroup.getAddressList().split(","); for (String item: addresss) { if (StringTool.isBlank(item)) { return Response.ofFail(I18nUtil.getString("jobgroup_field_registryList_unvalid") ); } if (!(HttpTool.isHttp(item) || HttpTool.isHttps(item))) { return Response.ofFail( I18nUtil.getString("jobgroup_field_registryList_unvalid")+"[2]" ); } } } // process xxlJobGroup.setUpdateTime(new Date()); int ret = xxlJobGroupMapper.update(xxlJobGroup); return (ret>0)?Response.ofSuccess():Response.ofFail(); } private List findRegistryByAppName(String appnameParam){ HashMap> appAddressMap = new HashMap<>(); List list = xxlJobRegistryMapper.findAll(Const.DEAD_TIMEOUT, new Date()); if (CollectionTool.isNotEmpty(list)) { for (XxlJobRegistry item: list) { if (!RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { continue; } String appname = item.getRegistryKey(); List registryList = appAddressMap.computeIfAbsent(appname, k -> new ArrayList<>()); if (!registryList.contains(item.getRegistryValue())) { registryList.add(item.getRegistryValue()); } } } return appAddressMap.get(appnameParam); } @RequestMapping("/delete") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response delete(@RequestParam("ids[]") List ids){ // parse id if (CollectionTool.isEmpty(ids) || ids.size()!=1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("system_one") + I18nUtil.getString("system_data")); } int id = ids.get(0); // valid repeat operation XxlJobGroup xxlJobGroup = xxlJobGroupMapper.load(id); if (xxlJobGroup == null) { return Response.ofSuccess(); } // whether exists job int count = xxlJobInfoMapper.pageListCount(0, 10, id, -1, null, null, null); if (count > 0) { return Response.ofFail( I18nUtil.getString("jobgroup_del_limit_0") ); } // whether only exists one group List allList = xxlJobGroupMapper.findAll(); if (allList.size() == 1) { return Response.ofFail( I18nUtil.getString("jobgroup_del_limit_1") ); } // remove group int ret = xxlJobGroupMapper.remove(id); // remove registry-data xxlJobRegistryMapper.removeByRegistryGroupAndKey(RegistType.EXECUTOR.name(), xxlJobGroup.getAppname()); return (ret>0)?Response.ofSuccess():Response.ofFail(); } @RequestMapping("/loadById") @ResponseBody //@XxlSso(role = Consts.ADMIN_ROLE) // open to default user, support show registry nodes public Response loadById(@RequestParam("id") int id){ XxlJobGroup jobGroup = xxlJobGroupMapper.load(id); return jobGroup!=null?Response.ofSuccess(jobGroup):Response.ofFail(); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobInfoController.java ================================================ package com.xxl.job.admin.controller.biz; import com.xxl.job.admin.mapper.XxlJobGroupMapper; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.scheduler.exception.XxlJobException; import com.xxl.job.admin.scheduler.misfire.MisfireStrategyEnum; import com.xxl.job.admin.scheduler.route.ExecutorRouteStrategyEnum; import com.xxl.job.admin.scheduler.type.ScheduleTypeEnum; import com.xxl.job.admin.service.XxlJobService; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.admin.util.JobGroupPermissionUtil; import com.xxl.job.core.constant.ExecutorBlockStrategyEnum; import com.xxl.job.core.glue.GlueTypeEnum; import com.xxl.sso.core.helper.XxlSsoHelper; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.DateTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; 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 java.util.ArrayList; import java.util.Date; import java.util.List; /** * 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 XxlJobGroupMapper xxlJobGroupMapper; @Resource private XxlJobService xxlJobService; @RequestMapping public String index(HttpServletRequest request, Model model, @RequestParam(value = "jobGroup", 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 jobGroupListTotal = xxlJobGroupMapper.findAll(); // filter group List jobGroupList = JobGroupPermissionUtil.filterJobGroupByPermission(request, jobGroupListTotal); if (CollectionTool.isEmpty(jobGroupList)) { throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); } // parse jobGroup if (!(CollectionTool.isNotEmpty(jobGroupList) && jobGroupList.stream().map(XxlJobGroup::getId).toList().contains(jobGroup))) { jobGroup = -1; } model.addAttribute("JobGroupList", jobGroupList); model.addAttribute("jobGroup", jobGroup); return "biz/job.list"; } @RequestMapping("/pageList") @ResponseBody public Response> pageList(HttpServletRequest request, @RequestParam(required = false, defaultValue = "0") int offset, @RequestParam(required = false, defaultValue = "10") int pagesize, @RequestParam int jobGroup, @RequestParam int triggerStatus, @RequestParam String jobDesc, @RequestParam String executorHandler, @RequestParam String author) { // valid jobGroup permission JobGroupPermissionUtil.validJobGroupPermission(request, jobGroup); // page return xxlJobService.pageList(offset, pagesize, jobGroup, triggerStatus, jobDesc, executorHandler, author); } @RequestMapping("/insert") @ResponseBody public Response add(HttpServletRequest request, XxlJobInfo jobInfo) { // valid permission LoginInfo loginInfo = JobGroupPermissionUtil.validJobGroupPermission(request, jobInfo.getJobGroup()); // opt return xxlJobService.add(jobInfo, loginInfo); } @RequestMapping("/update") @ResponseBody public Response update(HttpServletRequest request, XxlJobInfo jobInfo) { // valid permission LoginInfo loginInfo = JobGroupPermissionUtil.validJobGroupPermission(request, jobInfo.getJobGroup()); // opt return xxlJobService.update(jobInfo, loginInfo); } @RequestMapping("/delete") @ResponseBody public Response delete(HttpServletRequest request, @RequestParam("ids[]") List ids) { // valid if (CollectionTool.isEmpty(ids) || ids.size()!=1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("system_one") + I18nUtil.getString("system_data")); } // invoke Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); return xxlJobService.remove(ids.get(0), loginInfoResponse.getData()); } @RequestMapping("/stop") @ResponseBody public Response pause(HttpServletRequest request, @RequestParam("ids[]") List ids) { // valid if (CollectionTool.isEmpty(ids) || ids.size()!=1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("system_one") + I18nUtil.getString("system_data")); } // invoke Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); return xxlJobService.stop(ids.get(0), loginInfoResponse.getData()); } @RequestMapping("/start") @ResponseBody public Response start(HttpServletRequest request, @RequestParam("ids[]") List ids) { // valid if (CollectionTool.isEmpty(ids) || ids.size()!=1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("system_one") + I18nUtil.getString("system_data")); } // invoke Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); return xxlJobService.start(ids.get(0), loginInfoResponse.getData()); } @RequestMapping("/trigger") @ResponseBody public Response triggerJob(HttpServletRequest request, @RequestParam("id") int id, @RequestParam("executorParam") String executorParam, @RequestParam("addressList") String addressList) { Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); return xxlJobService.trigger(loginInfoResponse.getData(), id, executorParam, addressList); } @RequestMapping("/nextTriggerTime") @ResponseBody public Response> nextTriggerTime(@RequestParam("scheduleType") String scheduleType, @RequestParam("scheduleConf") String scheduleConf) { // valid if (StringTool.isBlank(scheduleType) || StringTool.isBlank(scheduleConf)) { return Response.ofSuccess(new ArrayList<>()); } // param XxlJobInfo paramXxlJobInfo = new XxlJobInfo(); paramXxlJobInfo.setScheduleType(scheduleType); paramXxlJobInfo.setScheduleConf(scheduleConf); // generate List result = new ArrayList<>(); try { Date lastTime = new Date(); for (int i = 0; i < 5; i++) { // generate next trigger time ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(paramXxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE); lastTime = scheduleTypeEnum.getScheduleType().generateNextTriggerTime(paramXxlJobInfo, lastTime); // collect data if (lastTime != null) { result.add(DateTool.formatDateTime(lastTime)); } else { break; } } } catch (Exception e) { logger.error(">>>>>>>>>>> nextTriggerTime error. scheduleType = {}, scheduleConf= {}, error:{} ", scheduleType, scheduleConf, e.getMessage()); return Response.ofFail((I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage()); } return Response.ofSuccess(result); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobLogController.java ================================================ package com.xxl.job.admin.controller.biz; import com.xxl.job.admin.mapper.XxlJobGroupMapper; import com.xxl.job.admin.mapper.XxlJobInfoMapper; import com.xxl.job.admin.mapper.XxlJobLogMapper; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.exception.XxlJobException; import com.xxl.job.admin.service.XxlJobService; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.admin.util.JobGroupPermissionUtil; import com.xxl.job.core.context.XxlJobContext; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.model.KillRequest; import com.xxl.job.core.openapi.model.LogRequest; import com.xxl.job.core.openapi.model.LogResult; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.DateTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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 org.springframework.web.util.HtmlUtils; 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 final Logger logger = LoggerFactory.getLogger(JobLogController.class); @Resource private XxlJobGroupMapper xxlJobGroupMapper; @Resource public XxlJobInfoMapper xxlJobInfoMapper; @Resource public XxlJobLogMapper xxlJobLogMapper; @Autowired private XxlJobService xxlJobService; @RequestMapping public String index(HttpServletRequest request, Model model, @RequestParam(value = "jobGroup", required = false, defaultValue = "0") Integer jobGroup, @RequestParam(value = "jobId", required = false, defaultValue = "0") Integer jobId) { // find all jobGroup List jobGroupListTotal = xxlJobGroupMapper.findAll(); // filter JobGroupList List jobGroupList = JobGroupPermissionUtil.filterJobGroupByPermission(request, jobGroupListTotal); if (CollectionTool.isEmpty(jobGroupList)) { throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); } // parse jobGroup if (jobId > 0) { // assign jobId (+ jobGroup) XxlJobInfo jobInfo = xxlJobInfoMapper.loadById(jobId); if (jobInfo == null) { // jobId not exist, inteceptor throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid")); } jobGroup = jobInfo.getJobGroup(); } else if (jobGroup > 0) { // assign jobGroup Integer finalJobGroup = jobGroup; if (CollectionTool.isEmpty(jobGroupListTotal.stream().filter(item -> item.getId() == finalJobGroup).toList())) { // jobGroup not exist, use first jobGroup = jobGroupList.get(0).getId(); } jobId = 0; } else { // default first valid jobGroup jobGroup = jobGroupList.get(0).getId(); jobId = 0; } /*// valid permission JobGroupPermissionUtil.validJobGroupPermission(request, jobGroup);*/ // find jobList List jobInfoList = xxlJobInfoMapper.getJobsByGroup(jobGroup); // parse jobId if (CollectionTool.isEmpty(jobInfoList)) { jobId = 0; } else { if (!jobInfoList.stream().map(XxlJobInfo::getId).toList().contains(jobId)) { // jobId not exist, use first jobId = jobInfoList.get(0).getId(); } } // write model.addAttribute("JobGroupList", jobGroupList); model.addAttribute("jobInfoList", jobInfoList); model.addAttribute("jobGroup", jobGroup); model.addAttribute("jobId", jobId); return "biz/log.list"; } @RequestMapping("/pageList") @ResponseBody public Response> pageList(HttpServletRequest request, @RequestParam(required = false, defaultValue = "0") int offset, @RequestParam(required = false, defaultValue = "10") int pagesize, @RequestParam int jobGroup, @RequestParam int jobId, @RequestParam int logStatus, @RequestParam String filterTime) { // valid jobGroup permission JobGroupPermissionUtil.validJobGroupPermission(request, jobGroup); // valid jobId if (jobId < 1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("jobinfo_job")); } // parse param Date triggerTimeStart = null; Date triggerTimeEnd = null; if (StringTool.isNotBlank(filterTime)) { String[] temp = filterTime.split(" - "); if (temp.length == 2) { triggerTimeStart = DateTool.parseDateTime(temp[0]); triggerTimeEnd = DateTool.parseDateTime(temp[1]); } } // page query List list = xxlJobLogMapper.pageList(offset, pagesize, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); int list_count = xxlJobLogMapper.pageListCount(offset, pagesize, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); // package result PageModel pageModel = new PageModel<>(); pageModel.setData(list); pageModel.setTotal(list_count); return Response.ofSuccess(pageModel); } /** * filter xss tag */ private String filter(String originData){ // exclude tag Map excludeTagMap = new HashMap(); excludeTagMap.put("
", "###TAG_BR###"); excludeTagMap.put("", "###TAG_BOLD###"); excludeTagMap.put("", "###TAG_BOLD_END###"); // replace for (String key : excludeTagMap.keySet()) { String value = excludeTagMap.get(key); originData = originData.replaceAll(key, value); } // htmlEscape originData = HtmlUtils.htmlEscape(originData, "UTF-8"); // replace back for (String key : excludeTagMap.keySet()) { String value = excludeTagMap.get(key); originData = originData.replaceAll(value, key); } return originData; } @RequestMapping("/logKill") @ResponseBody public Response logKill(HttpServletRequest request, @RequestParam("id") long id){ // base check XxlJobLog log = xxlJobLogMapper.load(id); XxlJobInfo jobInfo = xxlJobInfoMapper.loadById(log.getJobId()); if (jobInfo==null) { return Response.ofFail(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); } if (XxlJobContext.HANDLE_CODE_SUCCESS != log.getTriggerCode()) { return Response.ofFail( I18nUtil.getString("joblog_kill_log_limit")); } // valid JobGroup permission JobGroupPermissionUtil.validJobGroupPermission(request, jobInfo.getJobGroup()); // request of kill Response runResult = null; try { ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(log.getExecutorAddress()); runResult = executorBiz.kill(new KillRequest(jobInfo.getId())); } catch (Exception e) { logger.error(e.getMessage(), e); runResult = Response.ofFail( e.getMessage()); } if (XxlJobContext.HANDLE_CODE_SUCCESS == runResult.getCode()) { log.setHandleCode(XxlJobContext.HANDLE_CODE_FAIL); log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():"")); log.setHandleTime(new Date()); XxlJobAdminBootstrap.getInstance().getJobCompleter().complete(log); return Response.ofSuccess(runResult.getMsg()); } else { return Response.ofFail(runResult.getMsg()); } } @RequestMapping("/clearLog") @ResponseBody public Response clearLog(HttpServletRequest request, @RequestParam("jobGroup") int jobGroup, @RequestParam("jobId") int jobId, @RequestParam("type") int type){ // valid JobGroup permission JobGroupPermissionUtil.validJobGroupPermission(request, jobGroup); // valid jobId if (jobId < 1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("jobinfo_job")); } // opt Date clearBeforeTime = null; int clearBeforeNum = 0; if (type == 1) { clearBeforeTime = DateTool.addMonths(new Date(), -1); // 清理一个月之前日志数据 } else if (type == 2) { clearBeforeTime = DateTool.addMonths(new Date(), -3); // 清理三个月之前日志数据 } else if (type == 3) { clearBeforeTime = DateTool.addMonths(new Date(), -6); // 清理六个月之前日志数据 } else if (type == 4) { clearBeforeTime = DateTool.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 Response.ofFail(I18nUtil.getString("joblog_clean_type_unvalid")); } List logIds = null; do { logIds = xxlJobLogMapper.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000); if (logIds!=null && !logIds.isEmpty()) { xxlJobLogMapper.clearLog(logIds); } } while (logIds!=null && !logIds.isEmpty()); return Response.ofSuccess(); } @RequestMapping("/logDetailPage") public String logDetailPage(HttpServletRequest request, @RequestParam("id") long id, Model model){ // base check XxlJobLog jobLog = xxlJobLogMapper.load(id); if (jobLog == null) { throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid")); } // valid permission JobGroupPermissionUtil.validJobGroupPermission(request, jobLog.getJobGroup()); // load jobInfo XxlJobInfo jobInfo = xxlJobInfoMapper.loadById(jobLog.getJobId()); // data model.addAttribute("triggerCode", jobLog.getTriggerCode()); model.addAttribute("handleCode", jobLog.getHandleCode()); model.addAttribute("logId", jobLog.getId()); model.addAttribute("jobInfo", jobInfo); return "biz/log.detail"; } @RequestMapping("/logDetailCat") @ResponseBody public Response logDetailCat(@RequestParam("logId") long logId, @RequestParam("fromLineNum") int fromLineNum){ try { // valid XxlJobLog jobLog = xxlJobLogMapper.load(logId); // todo, need to improve performance if (jobLog == null) { return Response.ofFail(I18nUtil.getString("joblog_logid_unvalid")); } // log cat ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(jobLog.getExecutorAddress()); Response logResult = executorBiz.log(new LogRequest(jobLog.getTriggerTime().getTime(), logId, fromLineNum)); // is end if (logResult.getData()!=null && logResult.getData().getFromLineNum() > logResult.getData().getToLineNum()) { if (jobLog.getHandleCode() > 0) { logResult.getData().setEnd(true); } } // fix xss if (logResult.getData()!=null && StringTool.isNotBlank(logResult.getData().getLogContent())) { String newLogContent = filter(logResult.getData().getLogContent()); logResult.getData().setLogContent(newLogContent); } return logResult; } catch (Exception e) { logger.error("logId({}) logDetailCat error: {}", logId, e.getMessage(), e); return Response.ofFail(e.getMessage()); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobUserController.java ================================================ package com.xxl.job.admin.controller.biz; import com.xxl.job.admin.constant.Consts; import com.xxl.job.admin.mapper.XxlJobGroupMapper; import com.xxl.job.admin.mapper.XxlJobUserMapper; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobUser; import com.xxl.job.admin.util.I18nUtil; import com.xxl.sso.core.annotation.XxlSso; import com.xxl.sso.core.helper.XxlSsoHelper; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.crypto.Sha256Tool; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; 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 java.util.HashMap; import java.util.List; import java.util.Map; /** * @author xuxueli 2019-05-04 16:39:50 */ @Controller @RequestMapping("/user") public class JobUserController { @Resource private XxlJobUserMapper xxlJobUserMapper; @Resource private XxlJobGroupMapper xxlJobGroupMapper; @RequestMapping @XxlSso(role = Consts.ADMIN_ROLE) public String index(Model model) { // 执行器列表 List groupList = xxlJobGroupMapper.findAll(); model.addAttribute("groupList", groupList); return "biz/user.list"; } @RequestMapping("/pageList") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response> pageList(@RequestParam(required = false, defaultValue = "0") int offset, @RequestParam(required = false, defaultValue = "10") int pagesize, @RequestParam String username, @RequestParam int role) { // page list List list = xxlJobUserMapper.pageList(offset, pagesize, username, role); int list_count = xxlJobUserMapper.pageListCount(offset, pagesize, username, role); // filter if (list!=null && !list.isEmpty()) { for (XxlJobUser item: list) { item.setPassword(null); } } // package result PageModel pageModel = new PageModel<>(); pageModel.setData(list); pageModel.setTotal(list_count); return Response.ofSuccess(pageModel); } @RequestMapping("/insert") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response insert(XxlJobUser xxlJobUser) { // valid username if (StringTool.isBlank(xxlJobUser.getUsername())) { return Response.ofFail(I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") ); } xxlJobUser.setUsername(xxlJobUser.getUsername().trim()); if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) { return Response.ofFail(I18nUtil.getString("system_lengh_limit")+"[4-20]" ); } // valid password if (StringTool.isBlank(xxlJobUser.getPassword())) { return Response.ofFail(I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") ); } xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { return Response.ofFail(I18nUtil.getString("system_lengh_limit")+"[4-20]" ); } // md5 password String passwordHash = Sha256Tool.sha256(xxlJobUser.getPassword()); xxlJobUser.setPassword(passwordHash); // check repeat XxlJobUser existUser = xxlJobUserMapper.loadByUserName(xxlJobUser.getUsername()); if (existUser != null) { return Response.ofFail( I18nUtil.getString("user_username_repeat") ); } // write xxlJobUserMapper.save(xxlJobUser); return Response.ofSuccess(); } @RequestMapping("/update") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response update(HttpServletRequest request, XxlJobUser xxlJobUser) { // avoid opt login seft Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); if (loginInfoResponse.getData().getUserName().equals(xxlJobUser.getUsername())) { return Response.ofFail(I18nUtil.getString("user_update_loginuser_limit")); } // valid password if (StringTool.isNotBlank(xxlJobUser.getPassword())) { xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { return Response.ofFail(I18nUtil.getString("system_lengh_limit")+"[4-20]" ); } // md5 password String passwordHash = Sha256Tool.sha256(xxlJobUser.getPassword()); xxlJobUser.setPassword(passwordHash); } else { xxlJobUser.setPassword(null); } // write xxlJobUserMapper.update(xxlJobUser); return Response.ofSuccess(); } @RequestMapping("/delete") @ResponseBody @XxlSso(role = Consts.ADMIN_ROLE) public Response delete(HttpServletRequest request, @RequestParam("ids[]") List ids) { // valid if (CollectionTool.isEmpty(ids) || ids.size()!=1) { return Response.ofFail(I18nUtil.getString("system_please_choose") + I18nUtil.getString("system_one") + I18nUtil.getString("system_data")); } // avoid opt login seft Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); if (ids.contains(Integer.parseInt(loginInfoResponse.getData().getUserId()))) { return Response.ofFail(I18nUtil.getString("user_update_loginuser_limit")); } xxlJobUserMapper.delete(ids.get(0)); return Response.ofSuccess(); } /*@RequestMapping("/updatePwd") @ResponseBody public Response updatePwd(HttpServletRequest request, @RequestParam("password") String password, @RequestParam("oldPassword") String oldPassword){ // valid if (oldPassword==null || oldPassword.trim().isEmpty()){ return Response.ofFail(I18nUtil.getString("system_please_input") + I18nUtil.getString("change_pwd_field_oldpwd")); } if (password==null || password.trim().isEmpty()){ return Response.ofFail(I18nUtil.getString("system_please_input") + I18nUtil.getString("change_pwd_field_oldpwd")); } password = password.trim(); if (!(password.length()>=4 && password.length()<=20)) { return Response.ofFail(I18nUtil.getString("system_lengh_limit")+"[4-20]" ); } // md5 password String oldPasswordHash = Sha256Tool.sha256(oldPassword); String passwordHash = Sha256Tool.sha256(password); // valid old pwd Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); XxlJobUser existUser = xxlJobUserMapper.loadByUserName(loginInfoResponse.getData().getUserName()); if (!oldPasswordHash.equals(existUser.getPassword())) { return Response.ofFail(I18nUtil.getString("change_pwd_field_oldpwd") + I18nUtil.getString("system_unvalid")); } // write new existUser.setPassword(passwordHash); xxlJobUserMapper.update(existUser); return Response.ofSuccess(); }*/ } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobGroupMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.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 XxlJobGroupMapper { public List findAll(); public List 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 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); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobInfoMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.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 XxlJobInfoMapper { public List 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 getJobsByGroup(@Param("jobGroup") int jobGroup); public int findAllCount(); /** * find schedule job, limit "trigger_status = 1" * * @param maxNextTime * @param pagesize * @return */ public List scheduleJobQuery(@Param("maxNextTime") long maxNextTime, @Param("pagesize") int pagesize ); /** * update schedule job * * 1、can only update "trigger_status = 1", Avoid stopping tasks from being opened * 2、valid "triggerStatus gte 0", filter illegal state * * @param xxlJobInfo * @return */ public int scheduleUpdate(XxlJobInfo xxlJobInfo); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobLockMapper.java ================================================ package com.xxl.job.admin.mapper; import org.apache.ibatis.annotations.Mapper; /** * job lock * * @author xuxueli 2016-1-12 18:03:45 */ @Mapper public interface XxlJobLockMapper { /** * get schedule lock */ String scheduleLock(); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobLogGlueMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.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 XxlJobLogGlueMapper { public int save(XxlJobLogGlue xxlJobLogGlue); public List findByJobId(@Param("jobId") int jobId); public int removeOld(@Param("jobId") int jobId, @Param("limit") int limit); public int deleteByJobId(@Param("jobId") int jobId); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobLogMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.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 XxlJobLogMapper { // exist jobId not use jobGroup, not exist use jobGroup public List 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 findLogReport(@Param("from") Date from, @Param("to") Date to); public List 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 logIds); public List findFailJobLogIds(@Param("pagesize") int pagesize); public int updateAlarmStatus(@Param("logId") long logId, @Param("oldAlarmStatus") int oldAlarmStatus, @Param("newAlarmStatus") int newAlarmStatus); public List findLostJobIds(@Param("losedTime") Date losedTime); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobLogReportMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.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 XxlJobLogReportMapper { /*public int save(XxlJobLogReport xxlJobLogReport); public int update(XxlJobLogReport xxlJobLogReport);*/ public int saveOrUpdate(XxlJobLogReport xxlJobLogReport); public List queryLogReport(@Param("triggerDayFrom") Date triggerDayFrom, @Param("triggerDayTo") Date triggerDayTo); public XxlJobLogReport queryLogReportTotal(); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobRegistryMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.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 XxlJobRegistryMapper { public List findDead(@Param("timeout") int timeout, @Param("nowTime") Date nowTime); public int removeDead(@Param("ids") List ids); public List findAll(@Param("timeout") int timeout, @Param("nowTime") Date nowTime); public int registrySaveOrUpdate(@Param("registryGroup") String registryGroup, @Param("registryKey") String registryKey, @Param("registryValue") String registryValue, @Param("updateTime") Date updateTime); /*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); public int removeByRegistryGroupAndKey(@Param("registryGroup") String registryGroup, @Param("registryKey") String registryKey); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/mapper/XxlJobUserMapper.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobUser; import com.xxl.tool.response.Response; 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 XxlJobUserMapper { public List 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 XxlJobUser loadById(@Param("id") int id); public int save(XxlJobUser xxlJobUser); public int update(XxlJobUser xxlJobUser); public int delete(@Param("id") int id); public int updateToken(@Param("id") int id, @Param("token") String token); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobGroup.java ================================================ package com.xxl.job.admin.model; import com.xxl.tool.core.StringTool; 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 registryList; // 执行器地址列表(系统注册) public List getRegistryList() { if (StringTool.isNotBlank(addressList)) { registryList = new ArrayList<>(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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobInfo.java ================================================ package com.xxl.job.admin.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; // 调度类型:ScheduleTypeEnum private String scheduleConf; // 调度配置,值含义取决于调度类型 private String misfireStrategy; // 调度过期策略:MisfireStrategyEnum private String executorRouteStrategy; // 执行器路由策略:ExecutorRouteStrategyEnum private String executorHandler; // 执行器,任务Handler名称 private String executorParam; // 执行器,任务参数 private String executorBlockStrategy; // 阻塞处理策略:ExecutorBlockStrategyEnum private int executorTimeout; // 任务执行超时时间,单位秒 private int executorFailRetryCount; // 失败重试次数 private String glueType; // GLUE类型:GlueTypeEnum private String glueSource; // GLUE源代码 private String glueRemark; // GLUE备注 private Date glueUpdatetime; // GLUE更新时间 private String childJobId; // 子任务ID,多个逗号分隔 private int triggerStatus; // 调度状态:TriggerStatus 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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobLog.java ================================================ package com.xxl.job.admin.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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobLogGlue.java ================================================ package com.xxl.job.admin.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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobLogReport.java ================================================ package com.xxl.job.admin.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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobRegistry.java ================================================ package com.xxl.job.admin.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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/XxlJobUser.java ================================================ package com.xxl.job.admin.model; /** * @author xuxueli 2019-05-04 16:43:12 */ public class XxlJobUser { private int id; private String username; // 账号 private String password; // 密码 private String token; // 登录token 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 String getToken() { return token; } public void setToken(String token) { this.token = token; } 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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/model/dto/XxlBootResourceDTO.java ================================================ package com.xxl.job.admin.model.dto; import java.io.Serializable; import java.util.Date; import java.util.List; /** * XxlBootResource DTO * * Created by xuxueli on 2024-08-04 */ public class XxlBootResourceDTO implements Serializable { private static final long serialVersionUID = 42L; /** * 资源ID */ private int id; /** * 父节点ID */ private int parentId; /** * 名称 */ private String name; /** * 类型:0-目录, 1-菜单, 2-按钮 */ private int type; /** * 权限标识 */ private String permission; /** * 菜单地址 */ private String url; /** * ICON */ private String icon; /** * 顺序 */ private int order; /** * 状态:0-正常、1-禁用 */ private int status; /** * 新增时间 */ private Date addTime; /** * 更新时间 */ private Date updateTime; /** * child data */ private List children; public XxlBootResourceDTO() { } public XxlBootResourceDTO(int id, int parentId, String name, int type, String permission, String url, String icon, int order, int status, List children) { this.id = id; this.parentId = parentId; this.name = name; this.type = type; this.permission = permission; this.url = url; this.icon = icon; this.order = order; this.status = status; this.children = children; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getParentId() { return parentId; } public void setParentId(int parentId) { this.parentId = parentId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getType() { return type; } public void setType(int type) { this.type = type; } public String getPermission() { return permission; } public void setPermission(String permission) { this.permission = permission; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getIcon() { return icon; } public void setIcon(String icon) { this.icon = icon; } public int getOrder() { return order; } public void setOrder(int order) { this.order = order; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } 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 List getChildren() { return children; } public void setChildren(List children) { this.children = children; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/alarm/JobAlarm.java ================================================ package com.xxl.job.admin.scheduler.alarm; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; /** * @author xuxueli 2020-01-19 */ public interface JobAlarm { /** * job alarm * * @param info * @param jobLog * @return */ public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/alarm/JobAlarmer.java ================================================ package com.xxl.job.admin.scheduler.alarm; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.MapTool; 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; /** * xxl-job alarmer * * @author xuxueli 17/7/13. */ @Component public class JobAlarmer implements ApplicationContextAware, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(JobAlarmer.class); private ApplicationContext applicationContext; private List jobAlarmList; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public void afterPropertiesSet() throws Exception { Map serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class); if (MapTool.isNotEmpty(serviceBeanMap)) { jobAlarmList = new ArrayList<>(serviceBeanMap.values()); } } /** * job alarm */ public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) { boolean result = false; if (CollectionTool.isNotEmpty(jobAlarmList)) { 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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/alarm/impl/EmailJobAlarm.java ================================================ package com.xxl.job.admin.scheduler.alarm.impl; import com.xxl.job.admin.scheduler.alarm.JobAlarm; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.core.context.XxlJobContext; import jakarta.mail.internet.MimeMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; 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().isEmpty()) { // alarmContent String alarmContent = "Alarm Job LogId=" + jobLog.getId(); if (jobLog.getTriggerCode() != XxlJobContext.HANDLE_CODE_SUCCESS) { alarmContent += "
TriggerMsg=
" + jobLog.getTriggerMsg(); } if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != XxlJobContext.HANDLE_CODE_SUCCESS) { alarmContent += "
HandleCode=" + jobLog.getHandleMsg(); } // email info XxlJobGroup group = XxlJobAdminBootstrap.getInstance().getXxlJobGroupMapper().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 emailSet = new HashSet(Arrays.asList(info.getAlarmEmail().split(","))); for (String email: emailSet) { // make mail try { MimeMessage mimeMessage = XxlJobAdminBootstrap.getInstance().getMailSender().createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setFrom(XxlJobAdminBootstrap.getInstance().getEmailFrom(), personal); helper.setTo(email); helper.setSubject(title); helper.setText(content, true); XxlJobAdminBootstrap.getInstance().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 = "
" + I18nUtil.getString("jobconf_monitor_detail") + ":" + "\n" + " " + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
"+ I18nUtil.getString("jobinfo_field_jobgroup") +""+ I18nUtil.getString("jobinfo_field_id") +""+ I18nUtil.getString("jobinfo_field_jobdesc") +""+ I18nUtil.getString("jobconf_monitor_alarm_title") +""+ I18nUtil.getString("jobconf_monitor_alarm_content") +"
{0}{1}{2}"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"{3}
"; return mailBodyTemplate; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/complete/JobCompleter.java ================================================ package com.xxl.job.admin.scheduler.complete; import com.xxl.job.admin.mapper.XxlJobInfoMapper; import com.xxl.job.admin.mapper.XxlJobLogMapper; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.trigger.TriggerTypeEnum; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.core.context.XxlJobContext; import com.xxl.tool.core.StringTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.text.MessageFormat; /** * xxl-job job log complete * * @author xuxueli 2020-10-30 20:43:10 */ @Component public class JobCompleter { private static final Logger logger = LoggerFactory.getLogger(JobCompleter.class); @Resource private XxlJobInfoMapper xxlJobInfoMapper; @Resource private XxlJobLogMapper xxlJobLogMapper; /** * complate job (limit only once) */ public int complete(XxlJobLog xxlJobLog) { // 1、process child-job processChildJob(xxlJobLog); // text最大64kb 避免长度过长 if (xxlJobLog.getHandleMsg().length() > 15000) { xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) ); } // 2、fix_delay trigger next // on the way // 3、update job handle-info return xxlJobLogMapper.updateHandleInfo(xxlJobLog); } /** * do somethind to finish job */ private void processChildJob(XxlJobLog xxlJobLog){ // 1、handle success, to trigger child job String triggerChildMsg = null; if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) { XxlJobInfo xxlJobInfo = xxlJobInfoMapper.loadById(xxlJobLog.getJobId()); // process child job if (xxlJobInfo!=null && StringTool.isNotBlank(xxlJobInfo.getChildJobId())) { triggerChildMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<<
"; String[] childJobIds = xxlJobInfo.getChildJobId().split(","); for (int i = 0; i < childJobIds.length; i++) { // process eath child int childJobId = (StringTool.isNotBlank(childJobIds[i]) && StringTool.isNumeric(childJobIds[i])) ?Integer.parseInt(childJobIds[i]) :-1; if (childJobId > 0) { // valid if (childJobId == xxlJobLog.getJobId()) { logger.debug(">>>>>>>>>>> xxl-job, XxlJobCompleter-finishJob ignore childJobId, childJobId {} is self.", childJobId); continue; } // trigger child job XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null); Response triggerChildResult = Response.ofSuccess(); // add msg triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"), (i+1), childJobIds.length, childJobIds[i], (triggerChildResult.isSuccess()?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]); } } } } // 2、append trigger-child message if (StringTool.isNotBlank(triggerChildMsg)) { xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg ); } } /*private static boolean isNumeric(String str){ try { int result = Integer.valueOf(str); return true; } catch (NumberFormatException e) { return false; } }*/ } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/config/XxlJobAdminBootstrap.java ================================================ package com.xxl.job.admin.scheduler.config; import com.xxl.job.admin.mapper.*; import com.xxl.job.admin.scheduler.alarm.JobAlarmer; import com.xxl.job.admin.scheduler.complete.JobCompleter; import com.xxl.job.admin.scheduler.thread.*; import com.xxl.job.admin.scheduler.trigger.JobTrigger; import com.xxl.job.core.constant.Const; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.tool.core.StringTool; import com.xxl.tool.http.HttpTool; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 org.springframework.transaction.PlatformTransactionManager; import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * xxl-job config * * @author xuxueli 2017-04-28 */ @Component public class XxlJobAdminBootstrap implements InitializingBean, DisposableBean { private static final Logger logger = LoggerFactory.getLogger(XxlJobAdminBootstrap.class); // ---------------------- instance ---------------------- private static XxlJobAdminBootstrap adminConfig = null; public static XxlJobAdminBootstrap getInstance() { return adminConfig; } // ---------------------- start / stop ---------------------- @Override public void afterPropertiesSet() throws Exception { // init instance adminConfig = this; // start doStart(); } @Override public void destroy() throws Exception { // stop doStop(); } // job module private JobTriggerPoolHelper jobTriggerPoolHelper; private JobRegistryHelper jobRegistryHelper; private JobFailAlarmMonitorHelper jobFailAlarmMonitorHelper; private JobCompleteHelper jobCompleteHelper; private JobLogReportHelper jobLogReportHelper; private JobScheduleHelper jobScheduleHelper; public JobTriggerPoolHelper getJobTriggerPoolHelper() { return jobTriggerPoolHelper; } public JobRegistryHelper getJobRegistryHelper() { return jobRegistryHelper; } public JobCompleteHelper getJobCompleteHelper() { return jobCompleteHelper; } /** * do start */ private void doStart() throws Exception { // trigger-pool start jobTriggerPoolHelper = new JobTriggerPoolHelper(); jobTriggerPoolHelper.start(); // registry monitor start jobRegistryHelper = new JobRegistryHelper(); jobRegistryHelper.start(); // fail-alarm monitor start jobFailAlarmMonitorHelper = new JobFailAlarmMonitorHelper(); jobFailAlarmMonitorHelper.start(); // job complate start ( depend on JobTriggerPoolHelper ) for callback and result-lost jobCompleteHelper = new JobCompleteHelper(); jobCompleteHelper.start(); // log-report start jobLogReportHelper = new JobLogReportHelper(); jobLogReportHelper.start(); // job-schedule start ( depend on JobTriggerPoolHelper ) jobScheduleHelper = new JobScheduleHelper(); jobScheduleHelper.start(); logger.info(">>>>>>>>> xxl-job admin start success."); } /** * do stop */ private void doStop(){ // job-schedule stop jobScheduleHelper.stop(); // log-report stop jobLogReportHelper.stop(); // job complate stop jobCompleteHelper.stop(); // fail-alarm monitor stop jobFailAlarmMonitorHelper.stop(); // registry monitor stop jobRegistryHelper.stop(); // trigger-pool stop jobTriggerPoolHelper.stop(); logger.info(">>>>>>>>> xxl-job admin stopped."); } // ---------------------- executor-client ---------------------- private static ConcurrentMap executorBizRepository = new ConcurrentHashMap(); public static ExecutorBiz getExecutorBiz(String address) throws Exception { // valid if (StringTool.isBlank(address)) { return null; } // load-cache address = address.trim(); ExecutorBiz executorBiz = executorBizRepository.get(address); if (executorBiz != null) { return executorBiz; } // set-cache executorBiz = HttpTool.createClient() .url(address) .timeout(XxlJobAdminBootstrap.getInstance().getTimeout() * 1000) .header(Const.XXL_JOB_ACCESS_TOKEN, XxlJobAdminBootstrap.getInstance().getAccessToken()) .proxy(ExecutorBiz.class); executorBizRepository.put(address, executorBiz); return executorBiz; } // ---------------------- field ---------------------- // conf @Value("${xxl.job.i18n}") private String i18n; @Value("${xxl.job.accessToken}") private String accessToken; @Value("${xxl.job.timeout}") private int timeout; @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; // service, mapper @Resource private XxlJobLogMapper xxlJobLogMapper; @Resource private XxlJobInfoMapper xxlJobInfoMapper; @Resource private XxlJobRegistryMapper xxlJobRegistryMapper; @Resource private XxlJobGroupMapper xxlJobGroupMapper; @Resource private XxlJobLogReportMapper xxlJobLogReportMapper; @Resource private XxlJobLockMapper xxlJobLockMapper; @Resource private JavaMailSender mailSender; /*@Resource private DataSource dataSource;*/ @Resource private PlatformTransactionManager transactionManager; @Resource private JobAlarmer jobAlarmer; @Resource private JobTrigger jobTrigger; @Resource private JobCompleter jobCompleter; public String getI18n() { if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) { return "zh_CN"; } return i18n; } public String getAccessToken() { return accessToken; } public int getTimeout() { return timeout; } 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 < 3) { return -1; // Limit greater than or equal to 3, otherwise close } return logretentiondays; } public XxlJobLogMapper getXxlJobLogMapper() { return xxlJobLogMapper; } public XxlJobInfoMapper getXxlJobInfoMapper() { return xxlJobInfoMapper; } public XxlJobRegistryMapper getXxlJobRegistryMapper() { return xxlJobRegistryMapper; } public XxlJobGroupMapper getXxlJobGroupMapper() { return xxlJobGroupMapper; } public XxlJobLogReportMapper getXxlJobLogReportMapper() { return xxlJobLogReportMapper; } public XxlJobLockMapper getXxlJobLockMapper() { return xxlJobLockMapper; } public JavaMailSender getMailSender() { return mailSender; } /*public DataSource getDataSource() { return dataSource; }*/ public PlatformTransactionManager getTransactionManager() { return transactionManager; } public JobAlarmer getJobAlarmer() { return jobAlarmer; } public JobTrigger getJobTrigger() { return jobTrigger; } public JobCompleter getJobCompleter() { return jobCompleter; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/cron/CronExpression.java ================================================ package com.xxl.job.admin.scheduler.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.Optional; 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". *

* Cron expressions are comprised of 6 required fields and one optional field * separated by white space. The fields respectively are described as follows: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Examples of cron expressions and their meanings.
Field Name Allowed Values Allowed Special Characters
Seconds 0-59 , - * /
Minutes 0-59 , - * /
Hours 0-23 , - * /
Day-of-month 1-31 , - * ? / L W
Month 0-11 or JAN-DEC , - * /
Day-of-Week 1-7 or SUN-SAT , - * ? / L #
Year (Optional) empty, 1970-2199 , - * /
*

* The '*' character is used to specify all values. For example, "*" * in the minute field means "every minute". *

*

* 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. *

* The '-' character is used to specify ranges For example "10-12" in * the hour field means "the hours 10, 11 and 12". *

* 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". *

*

* 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. *

*

* 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. When using the 'L' option, it is important not to * specify lists, or ranges of values, as you'll get confusing/unexpected results. *

*

* 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. *

*

* 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". *

*

* 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). *

* *

* The legal characters and the names of months and days of the week are not * case sensitive. * *

* NOTES: *

*
    *
  • 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). *
  • *
  • 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".
  • *
* * * @author Sharada Jambula, James House * @author Contributions from Mads Henderson * @author Refactoring from CronTrigger to CronExpression by Aaron Craven * * Borrowed from quartz v2.5.0 */ 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 int MAX_LAST_DAY_OFFSET = 30; protected static final int LAST_DAY_OFFSET_START = 32; // "L-30" protected static final int LAST_DAY_OFFSET_END = LAST_DAY_OFFSET_START + MAX_LAST_DAY_OFFSET; // 'L' protected static final Integer ALL_SPEC = ALL_SPEC_INT; protected static final Integer NO_SPEC = NO_SPEC_INT; protected static final Map monthMap = new HashMap<>(20); protected static final Map dayMap = new HashMap<>(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 seconds; protected transient TreeSet minutes; protected transient TreeSet hours; protected transient TreeSet daysOfMonth; protected transient TreeSet nearestWeekdays; protected transient TreeSet months; protected transient TreeSet daysOfWeek; protected transient TreeSet years; protected transient boolean lastDayOfWeek = false; protected transient int nthDayOfWeek = 0; protected transient boolean expressionParsed = false; public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; /** * Constructs a new CronExpression 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 * CronExpression */ 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("Could not parse expression!", ex); } 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 after 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 after the given date/time which does * not 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 CronExpression * will be resolved. */ public TimeZone getTimeZone() { if (timeZone == null) { timeZone = TimeZone.getDefault(); } return timeZone; } /** * Sets the time zone for which this CronExpression * will be resolved. */ public void setTimeZone(TimeZone timeZone) { this.timeZone = timeZone; } /** * Returns the string representation of the CronExpression * * @return a string representation of the CronExpression */ @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<>(); } if (minutes == null) { minutes = new TreeSet<>(); } if (hours == null) { hours = new TreeSet<>(); } if (daysOfMonth == null) { daysOfMonth = new TreeSet<>(); } if (nearestWeekdays == null) { nearestWeekdays = new TreeSet<>(); } if (months == null) { months = new TreeSet<>(); } if (daysOfWeek == null) { daysOfWeek = new TreeSet<>(); } if (years == null) { years = new TreeSet<>(); } int exprOn = SECOND; StringTokenizer exprsTok = new StringTokenizer(expression, " \t", false); if(exprsTok.countTokens() > 7) { throw new ParseException("Invalid expression has too many terms: " + expression, -1); } while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { String expr = exprsTok.nextToken().trim(); // 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 dow = getSet(DAY_OF_WEEK); TreeSet 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 + ")", 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) { if (!daysOfMonth.isEmpty() && daysOfMonth.last() == 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_WEEK) { addToSet(7, 7, 0, type); } if (type == DAY_OF_MONTH) { int dom = LAST_DAY_OFFSET_END; boolean nearestWeekday = false; if (s.length() > i) { c = s.charAt(i); if (c == '-') { ValueSet vs = getValue(0, s, i + 1); int offset = vs.value; if (offset > MAX_LAST_DAY_OFFSET) throw new ParseException("Offset from last day must be <= " + MAX_LAST_DAY_OFFSET, i + 1); dom -= offset; i = vs.pos; } if (s.length() > i) { c = s.charAt(i); if (c == 'W') { nearestWeekday = true; i++; } } } if (nearestWeekday) { nearestWeekdays.add(dom); } else { daysOfMonth.add(dom); } } 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 set = getSet(type); set.add(val); i++; return i; } if (c == 'W') { if (type != DAY_OF_MONTH) { 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); nearestWeekdays.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 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("nearestWeekdays: "); buf.append(getExpressionSetSummary(nearestWeekdays)); 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("NthDayOfWeek: "); buf.append(nthDayOfWeek); buf.append("\n"); buf.append("years: "); buf.append(getExpressionSetSummary(years)); buf.append("\n"); return buf.toString(); } protected String getExpressionSetSummary(java.util.Set set) { if (set.contains(NO_SPEC)) { return "?"; } if (set.contains(ALL_SPEC)) { return "*"; } StringBuilder buf = new StringBuilder(); Iterator 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 list) { if (list.contains(NO_SPEC)) { return "?"; } if (list.contains(ALL_SPEC)) { return "*"; } StringBuilder buf = new StringBuilder(); Iterator 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 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 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 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.isEmpty()) { 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.isEmpty()) { 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.isEmpty()) { 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 Optional smallestDay = findSmallestDay(day, mon, cl.get(Calendar.YEAR), daysOfMonth); Optional smallestDayForWeekday = findSmallestDay(day, mon, cl.get(Calendar.YEAR), nearestWeekdays); t = day; day = -1; if (smallestDayForWeekday.isPresent()) { day = smallestDayForWeekday.get(); 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; } } if (smallestDay.isPresent()) { if (day == -1 || smallestDay.get() < day) { day = smallestDay.get(); } } else if (day == -1) { day = 1; 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 = daysToAdd > 0; 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.isEmpty()) { 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 switching 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.isEmpty()) { 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.isEmpty()) { 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 CronExpression matches. */ public Date getTimeBefore(Date endTime) { // FUTURE_TODO: implement QUARTZ-423 return null; } /** * NOT YET IMPLEMENTED: Returns the final time that the * CronExpression 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 Optional findSmallestDay(int day, int mon, int year, TreeSet set) { if (set.isEmpty()) { return Optional.empty(); } final int lastDay = getLastDayOfMonth(mon, year); // For "L", "L-1", etc. int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day))) .map(d -> d - LAST_DAY_OFFSET_START + 1) .orElse(Integer.MAX_VALUE); // For "1", "2", etc. SortedSet st = set.subSet(day, LAST_DAY_OFFSET_START); // make sure we don't over-run a short month, such as february if (!st.isEmpty() && st.first() < smallestDay && st.first() <= lastDay) { smallestDay = st.first(); } return smallestDay == Integer.MAX_VALUE ? Optional.empty() : Optional.of(smallestDay); } 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; } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/exception/XxlJobException.java ================================================ package com.xxl.job.admin.scheduler.exception; /** * @author xuxueli 2019-05-04 23:19:29 */ public class XxlJobException extends RuntimeException { public XxlJobException() { } public XxlJobException(String message) { super(message); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/misfire/MisfireHandler.java ================================================ package com.xxl.job.admin.scheduler.misfire; /** * Misfire Handler * * @author xuxueli 2020-10-29 */ public abstract class MisfireHandler { /** * misfire handle * * @param jobId jobId */ public abstract void handle(final int jobId); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/misfire/MisfireStrategyEnum.java ================================================ package com.xxl.job.admin.scheduler.misfire; import com.xxl.job.admin.scheduler.misfire.strategy.MisfireDoNothing; import com.xxl.job.admin.scheduler.misfire.strategy.MisfireFireOnceNow; import com.xxl.job.admin.util.I18nUtil; /** * @author xuxueli 2020-10-29 21:11:23 */ public enum MisfireStrategyEnum { /** * do nothing */ DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing"), new MisfireDoNothing()), /** * fire once now */ FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now"), new MisfireFireOnceNow()); private final String title; private final MisfireHandler misfireHandler; MisfireStrategyEnum(String title, MisfireHandler misfireHandler) { this.title = title; this.misfireHandler = misfireHandler; } public String getTitle() { return title; } public MisfireHandler getMisfireHandler() { return misfireHandler; } /** * match misfire strategy * * @param name name of misfire strategy * @param defaultItem default misfire strategy * @return misfire strategy */ public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){ for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) { if (item.name().equals(name)) { return item; } } return defaultItem; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/misfire/strategy/MisfireDoNothing.java ================================================ package com.xxl.job.admin.scheduler.misfire.strategy; import com.xxl.job.admin.scheduler.misfire.MisfireHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MisfireDoNothing extends MisfireHandler { private static final Logger logger = LoggerFactory.getLogger(MisfireDoNothing.class); @Override public void handle(int jobId) { logger.warn(">>>>>>>>>>> xxl-job, schedule MisfireDoNothing: jobId = " + jobId ); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/misfire/strategy/MisfireFireOnceNow.java ================================================ package com.xxl.job.admin.scheduler.misfire.strategy; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.misfire.MisfireHandler; import com.xxl.job.admin.scheduler.trigger.TriggerTypeEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MisfireFireOnceNow extends MisfireHandler { protected static Logger logger = LoggerFactory.getLogger(MisfireFireOnceNow.class); @Override public void handle(int jobId) { // FIRE_ONCE_NOW 》 trigger XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.MISFIRE, -1, null, null, null); logger.warn(">>>>>>>>>>> xxl-job, schedule MisfireFireOnceNow: jobId = " + jobId ); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/openapi/OpenApiController.java ================================================ package com.xxl.job.admin.scheduler.openapi; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.core.constant.Const; import com.xxl.job.core.openapi.AdminBiz; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.openapi.model.RegistryRequest; import com.xxl.sso.core.annotation.XxlSso; import com.xxl.tool.core.StringTool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.List; /** * Created by xuxueli on 17/5/10. */ @Controller public class OpenApiController { @Resource private AdminBiz adminBiz; /** * api */ @RequestMapping("/api/{uri}") @ResponseBody @XxlSso(login = false) public Object api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestHeader(value = Const.XXL_JOB_ACCESS_TOKEN, required = false) String accesstoken, @RequestBody(required = false) String requestBody) { // valid if (!"POST".equalsIgnoreCase(request.getMethod())) { return Response.ofFail("invalid request, HttpMethod not support."); } if (StringTool.isBlank(uri)) { return Response.ofFail("invalid request, uri-mapping empty."); } if (StringTool.isBlank(requestBody)) { return Response.ofFail("invalid request, requestBody empty."); } // valid token if (StringTool.isNotBlank(XxlJobAdminBootstrap.getInstance().getAccessToken()) && !XxlJobAdminBootstrap.getInstance().getAccessToken().equals(accesstoken)) { return Response.ofFail("The access token is wrong."); } // dispatch request try { switch (uri) { case "callback": { List callbackParamList = GsonTool.fromJson(requestBody, List.class, CallbackRequest.class); return adminBiz.callback(callbackParamList); } case "registry": { RegistryRequest registryParam = GsonTool.fromJson(requestBody, RegistryRequest.class); return adminBiz.registry(registryParam); } case "registryRemove": { RegistryRequest registryParam = GsonTool.fromJson(requestBody, RegistryRequest.class); return adminBiz.registryRemove(registryParam); } default: return Response.ofFail("invalid request, uri-mapping("+ uri +") not found."); } } catch (Exception e) { return Response.ofFail("openapi invoke error: " + e.getMessage()); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/ExecutorRouteStrategyEnum.java ================================================ package com.xxl.job.admin.scheduler.route; import com.xxl.job.admin.scheduler.route.strategy.*; import com.xxl.job.admin.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; } /** * match 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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/ExecutorRouter.java ================================================ package com.xxl.job.admin.scheduler.route; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; 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 executor address list * @return ReturnT.content=address */ public abstract Response route(TriggerRequest triggerParam, List addressList); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteBusyover.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.model.IdleBeatRequest; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; import java.util.List; /** * Created by xuxueli on 17/3/10. */ public class ExecutorRouteBusyover extends ExecutorRouter { @Override public Response route(TriggerRequest triggerParam, List addressList) { StringBuffer idleBeatResultSB = new StringBuffer(); for (String address : addressList) { // beat Response idleBeatResult = null; try { ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(address); idleBeatResult = executorBiz.idleBeat(new IdleBeatRequest(triggerParam.getJobId())); } catch (Exception e) { logger.error(e.getMessage(), e); idleBeatResult = Response.ofFail( ""+e ); } idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"

":"") .append(I18nUtil.getString("jobconf_idleBeat") + ":") .append("
address:").append(address) .append("
code:").append(idleBeatResult.getCode()) .append("
msg:").append(idleBeatResult.getMsg()); // beat success if (idleBeatResult.isSuccess()) { idleBeatResult.setMsg(idleBeatResultSB.toString()); idleBeatResult.setData(address); return idleBeatResult; } } return Response.ofFail( idleBeatResultSB.toString()); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteConsistentHash.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; import java.nio.charset.StandardCharsets; 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 final int VIRTUAL_NODE_NUM = 100; /** * get hash code on 2^32 ring (md5散列的方式计算hash值) * * @param key key * @return hash code */ 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; keyBytes = key.getBytes(StandardCharsets.UTF_8); 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); return hashCode & 0xffffffffL; } /** * get address by jobId * * @param jobId job id * @param addressList address list * @return address */ public String hashJob(int jobId, List addressList) { // ------A1------A2-------A3------ // -----------J1------------------ TreeMap addressRing = new TreeMap(); 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 lastRing = addressRing.tailMap(jobHash); if (!lastRing.isEmpty()) { return lastRing.get(lastRing.firstKey()); } return addressRing.firstEntry().getValue(); } @Override public Response route(TriggerRequest triggerParam, List addressList) { String address = hashJob(triggerParam.getJobId(), addressList); return Response.ofSuccess(address); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteFailover.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; import java.util.List; /** * Created by xuxueli on 17/3/10. */ public class ExecutorRouteFailover extends ExecutorRouter { @Override public Response route(TriggerRequest triggerParam, List addressList) { StringBuffer beatResultSB = new StringBuffer(); for (String address : addressList) { // beat Response beatResult = null; try { ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(address); beatResult = executorBiz.beat(); } catch (Exception e) { logger.error(e.getMessage(), e); beatResult = Response.ofFail(e.getMessage() ); } beatResultSB.append( (beatResultSB.length()>0)?"

":"") .append(I18nUtil.getString("jobconf_beat") + ":") .append("
address:").append(address) .append("
code:").append(beatResult.getCode()) .append("
msg:").append(beatResult.getMsg()); // beat success if (beatResult.isSuccess()) { beatResult.setMsg(beatResultSB.toString()); beatResult.setData(address); return beatResult; } } return Response.ofFail( beatResultSB.toString()); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteFirst.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; import java.util.List; /** * Created by xuxueli on 17/3/10. */ public class ExecutorRouteFirst extends ExecutorRouter { @Override public Response route(TriggerRequest triggerParam, List addressList){ return Response.ofSuccess(addressList.get(0)); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteLFU.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; 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 { /** * job lfu map * * > */ private static ConcurrentMap> jobLfuMap = new ConcurrentHashMap>(); private static long CACHE_VALID_TIME = 0; public String route(int jobId, List addressList) { // cache clear if (System.currentTimeMillis() > CACHE_VALID_TIME) { jobLfuMap.clear(); CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; } // lfu item init HashMap lfuItemMap = jobLfuMap.get(jobId); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList; if (lfuItemMap == null) { lfuItemMap = new HashMap<>(); 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 delKeys = new ArrayList<>(); for (String existKey: lfuItemMap.keySet()) { if (!addressList.contains(existKey)) { delKeys.add(existKey); } } if (!delKeys.isEmpty()) { for (String delKey: delKeys) { lfuItemMap.remove(delKey); } } // load least userd count address List> lfuItemList = new ArrayList<>(lfuItemMap.entrySet()); lfuItemList.sort(Map.Entry.comparingByValue()); // 默认升序, 获取 Value 最小值 Map.Entry addressItem = lfuItemList.get(0); addressItem.setValue(addressItem.getValue() + 1); return addressItem.getKey(); } @Override public Response route(TriggerRequest triggerParam, List addressList) { String address = route(triggerParam.getJobId(), addressList); return Response.ofSuccess(address); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteLRU.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; 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 { /** * job lru map * * > */ private static ConcurrentMap> jobLRUMap = new ConcurrentHashMap>(); private static long CACHE_VALID_TIME = 0; public String route(int jobId, List addressList) { // cache clear if (System.currentTimeMillis() > CACHE_VALID_TIME) { jobLRUMap.clear(); CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; } // init lru LinkedHashMap lruItem = jobLRUMap.get(jobId); if (lruItem == null) { /** * LinkedHashMap * a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期; * b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法; */ lruItem = new LinkedHashMap<>(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 delKeys = new ArrayList<>(); for (String existKey: lruItem.keySet()) { if (!addressList.contains(existKey)) { delKeys.add(existKey); } } if (!delKeys.isEmpty()) { for (String delKey: delKeys) { lruItem.remove(delKey); } } // load first elment, eldest entry String eldestKey = lruItem.entrySet().iterator().next().getKey(); return lruItem.get(eldestKey); } @Override public Response route(TriggerRequest triggerParam, List addressList) { String address = route(triggerParam.getJobId(), addressList); return Response.ofSuccess(address); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteLast.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; import java.util.List; /** * Created by xuxueli on 17/3/10. */ public class ExecutorRouteLast extends ExecutorRouter { @Override public Response route(TriggerRequest triggerParam, List addressList) { return Response.ofSuccess(addressList.get(addressList.size()-1)); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteRandom.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; 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 Response route(TriggerRequest triggerParam, List addressList) { String address = addressList.get(localRandom.nextInt(addressList.size())); return Response.ofSuccess(address); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/route/strategy/ExecutorRouteRound.java ================================================ package com.xxl.job.admin.scheduler.route.strategy; import com.xxl.job.admin.scheduler.route.ExecutorRouter; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.response.Response; 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 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 Response route(TriggerRequest triggerParam, List addressList) { String address = addressList.get(count(triggerParam.getJobId())%addressList.size()); return Response.ofSuccess(address); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/thread/JobCompleteHelper.java ================================================ package com.xxl.job.admin.scheduler.thread; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.context.XxlJobContext; import com.xxl.tool.core.DateTool; import com.xxl.tool.response.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.List; import java.util.concurrent.*; /** * job complate, for callback and result-lost * * @author xuxueli 2015-9-1 18:05:56 */ public class JobCompleteHelper { private static final Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class); // ---------------------- monitor ---------------------- private ThreadPoolExecutor callbackThreadPool = null; private Thread monitorThread; private volatile boolean toStop = false; /** * start */ public void start(){ // for callback callbackThreadPool = new ThreadPoolExecutor( 2, 20, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue(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 (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } // monitor while (!toStop) { try { // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; Date losedTime = DateTool.addMinutes(new Date(), -10); List losedJobIds = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().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(XxlJobContext.HANDLE_CODE_FAIL); jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") ); XxlJobAdminBootstrap.getInstance().getJobCompleter().complete(jobLog); } } } catch (Throwable e) { if (!toStop) { logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); } } try { TimeUnit.SECONDS.sleep(60); } catch (Throwable 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(); } /** * stop */ public void stop(){ toStop = true; // stop registryOrRemoveThreadPool callbackThreadPool.shutdownNow(); // stop monitorThread (interrupt and wait) monitorThread.interrupt(); try { monitorThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } // ---------------------- helper ---------------------- public Response callback(List callbackParamList) { callbackThreadPool.execute(new Runnable() { @Override public void run() { for (CallbackRequest callbackRequest: callbackParamList) { Response callbackResult = doCallback(callbackRequest); logger.debug(">>>>>>>>> JobApiController.callback {}, callbackRequest={}, callbackResult={}", (callbackResult.isSuccess()?"success":"fail"), callbackRequest, callbackResult); } } }); return Response.ofSuccess(); } private Response doCallback(CallbackRequest handleCallbackParam) { // valid log item XxlJobLog log = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().load(handleCallbackParam.getLogId()); if (log == null) { return Response.ofFail( "log item not found."); } if (log.getHandleCode() > 0) { return Response.ofFail("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("
"); } if (handleCallbackParam.getHandleMsg() != null) { handleMsg.append(handleCallbackParam.getHandleMsg()); } // success, save log log.setHandleTime(new Date()); log.setHandleCode(handleCallbackParam.getHandleCode()); log.setHandleMsg(handleMsg.toString()); XxlJobAdminBootstrap.getInstance().getJobCompleter().complete(log); return Response.ofSuccess(); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/thread/JobFailAlarmMonitorHelper.java ================================================ package com.xxl.job.admin.scheduler.thread; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.trigger.TriggerTypeEnum; import com.xxl.job.admin.util.I18nUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.TimeUnit; /** * job fail-monitor helper * * @author xuxueli 2015-9-1 18:05:56 */ public class JobFailAlarmMonitorHelper { private static Logger logger = LoggerFactory.getLogger(JobFailAlarmMonitorHelper.class); // ---------------------- monitor ---------------------- private Thread monitorThread; private volatile boolean toStop = false; /** * start */ public void start(){ monitorThread = new Thread(new Runnable() { @Override public void run() { // monitor while (!toStop) { try { List failLogIds = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findFailJobLogIds(1000); if (failLogIds!=null && !failLogIds.isEmpty()) { for (long failLogId: failLogIds) { // lock log int lockRet = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().updateAlarmStatus(failLogId, 0, -1); if (lockRet < 1) { continue; } XxlJobLog log = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().load(failLogId); XxlJobInfo info = XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().loadById(log.getJobId()); // 1、fail retry monitor if (log.getExecutorFailRetryCount() > 0) { XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); String retryMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
"; log.setTriggerMsg(log.getTriggerMsg() + retryMsg); XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().updateTriggerInfo(log); } // 2、fail alarm monitor int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 if (info != null) { boolean alarmResult = XxlJobAdminBootstrap.getInstance().getJobAlarmer().alarm(info, log); newAlarmStatus = alarmResult?2:3; } else { newAlarmStatus = 1; } XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().updateAlarmStatus(failLogId, -1, newAlarmStatus); } } } catch (Throwable e) { if (!toStop) { logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e.getMessage(), e); } } try { TimeUnit.SECONDS.sleep(10); } catch (Throwable 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(); } /** * stop */ public void stop(){ toStop = true; // interrupt and wait monitorThread.interrupt(); try { monitorThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/thread/JobLogReportHelper.java ================================================ package com.xxl.job.admin.scheduler.thread; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.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 final Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class); private Thread logReportThread; private volatile boolean toStop = false; /** * start */ public void start(){ logReportThread = 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 triggerCountMap = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findLogReport(todayFrom, todayTo); if (triggerCountMap!=null && !triggerCountMap.isEmpty()) { int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.parseInt(String.valueOf(triggerCountMap.get("triggerDayCount"))):0; int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.parseInt(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0; int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.parseInt(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0; int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc; xxlJobLogReport.setRunningCount(triggerDayCountRunning); xxlJobLogReport.setSucCount(triggerDayCountSuc); xxlJobLogReport.setFailCount(triggerDayCountFail); } // do refresh: XxlJobAdminBootstrap.getInstance().getXxlJobLogReportMapper().saveOrUpdate(xxlJobLogReport); // 0-fail; 1-save suc; 2-update suc; /*if (ret < 1) { XxlJobAdminBootstrap.getInstance().getXxlJobLogReportMapper().save(xxlJobLogReport); }*/ } } catch (Throwable e) { if (!toStop) { logger.error(">>>>>>>>>>> xxl-job, JobLogReportHelper(log-report refresh) error:{}", e.getMessage(), e); } } // 2、log-clean: switch open & once each day try { if (XxlJobAdminBootstrap.getInstance().getLogretentiondays()>0 && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) { // expire-time Calendar expiredDay = Calendar.getInstance(); expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminBootstrap.getInstance().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 logIds = null; do { logIds = XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().findClearLogIds(0, 0, clearBeforeTime, 0, 1000); if (logIds!=null && !logIds.isEmpty()) { XxlJobAdminBootstrap.getInstance().getXxlJobLogMapper().clearLog(logIds); } } while (logIds!=null && !logIds.isEmpty()); // update clean time lastCleanLogTime = System.currentTimeMillis(); } } catch (Throwable e) { if (!toStop) { logger.error(">>>>>>>>>>> xxl-job, JobLogReportHelper(log-clean) error:{}", e.getMessage(), e); } } try { TimeUnit.MINUTES.sleep(1); } catch (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(">>>>>>>>>>> xxl-job, job log report thread stop"); } }); logReportThread.setDaemon(true); logReportThread.setName("xxl-job, admin JobLogReportHelper"); logReportThread.start(); } /** * stop */ public void stop(){ toStop = true; // interrupt and wait logReportThread.interrupt(); try { logReportThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/thread/JobRegistryHelper.java ================================================ package com.xxl.job.admin.scheduler.thread; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobRegistry; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.core.constant.RegistType; import com.xxl.job.core.openapi.model.RegistryRequest; import com.xxl.job.core.constant.Const; import com.xxl.tool.core.StringTool; import com.xxl.tool.response.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.*; /** * job registry instance helper * * @author xuxueli 2016-10-02 19:10:24 */ public class JobRegistryHelper { private static Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class); private ThreadPoolExecutor registryOrRemoveThreadPool = null; private Thread registryMonitorThread; private volatile boolean toStop = false; /** * start */ public void start(){ // for registry or remove registryOrRemoveThreadPool = new ThreadPoolExecutor( 2, 10, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue(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 groupList = XxlJobAdminBootstrap.getInstance().getXxlJobGroupMapper().findByAddressType(0); if (groupList!=null && !groupList.isEmpty()) { // remove dead address (admin/executor) List ids = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().findDead(Const.DEAD_TIMEOUT, new Date()); if (ids!=null && !ids.isEmpty()) { XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().removeDead(ids); } // fresh online address (admin/executor) HashMap> appAddressMap = new HashMap>(); List list = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().findAll(Const.DEAD_TIMEOUT, new Date()); if (list != null) { for (XxlJobRegistry item: list) { if (RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { String appname = item.getRegistryKey(); List registryList = appAddressMap.get(appname); if (registryList == null) { registryList = new ArrayList(); } if (!registryList.contains(item.getRegistryValue())) { registryList.add(item.getRegistryValue()); } appAddressMap.put(appname, registryList); } } } // fresh group address for (XxlJobGroup group: groupList) { List 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()); XxlJobAdminBootstrap.getInstance().getXxlJobGroupMapper().update(group); } } } catch (Throwable e) { if (!toStop) { logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); } } try { TimeUnit.SECONDS.sleep(Const.BEAT_TIMEOUT); } catch (Throwable 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(); } /** * stop */ public void stop(){ toStop = true; // stop registryOrRemoveThreadPool registryOrRemoveThreadPool.shutdownNow(); // stop monitor (interrupt and wait) registryMonitorThread.interrupt(); try { registryMonitorThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } // ---------------------- tool ---------------------- /** * registry */ public Response registry(RegistryRequest registryParam) { // valid if (StringTool.isBlank(registryParam.getRegistryGroup()) || StringTool.isBlank(registryParam.getRegistryKey()) || StringTool.isBlank(registryParam.getRegistryValue())) { return Response.ofFail("Illegal Argument."); } // async execute registryOrRemoveThreadPool.execute(new Runnable() { @Override public void run() { // 0-fail; 1-save suc; 2-update suc; int ret = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().registrySaveOrUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); if (ret == 1) { // fresh (add) freshGroupRegistryInfo(registryParam); } /*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 Response.ofSuccess(); } /** * registry remove */ public Response registryRemove(RegistryRequest registryParam) { // valid if (StringTool.isBlank(registryParam.getRegistryGroup()) || StringTool.isBlank(registryParam.getRegistryKey()) || StringTool.isBlank(registryParam.getRegistryValue())) { return Response.ofFail("Illegal Argument."); } // async execute registryOrRemoveThreadPool.execute(new Runnable() { @Override public void run() { int ret = XxlJobAdminBootstrap.getInstance().getXxlJobRegistryMapper().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue()); if (ret > 0) { // fresh (delete) freshGroupRegistryInfo(registryParam); } } }); return Response.ofSuccess(); } private void freshGroupRegistryInfo(RegistryRequest registryParam){ // Under consideration, prevent affecting core tables } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/thread/JobScheduleHelper.java ================================================ package com.xxl.job.admin.scheduler.thread; import com.xxl.job.admin.constant.TriggerStatus; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.misfire.MisfireStrategyEnum; import com.xxl.job.admin.scheduler.trigger.TriggerTypeEnum; import com.xxl.job.admin.scheduler.type.ScheduleTypeEnum; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.MapTool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** * @author xuxueli 2019-05-21 */ public class JobScheduleHelper { private static final Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class); /** * pre-read time for scheduler, increase efficiency */ public static final long PRE_READ_MS = 5000; /* * elegant shutdown wait seconds */ private static final long ELEGANT_SHUTDOWN_WAITING_SECONDS = 10; private Thread scheduleThread; private Thread ringThread; private volatile boolean scheduleThreadToStop = false; private volatile boolean ringThreadToStop = false; private final Map> ringData = new ConcurrentHashMap<>(); /** * start */ public void start(){ // schedule thread scheduleThread = new Thread(new Runnable() { @Override public void run() { // align time try { TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); } catch (Throwable 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 100ms, qps = 1000/100 = 100) int preReadCount = (XxlJobAdminBootstrap.getInstance().getTriggerPoolFastMax() + XxlJobAdminBootstrap.getInstance().getTriggerPoolSlowMax()) * 10; // do schedule while (!scheduleThreadToStop) { // param long start = System.currentTimeMillis(); boolean preReadSuc = true; // transaction start TransactionStatus transactionStatus = null; try { transactionStatus = XxlJobAdminBootstrap.getInstance().getTransactionManager().getTransaction(new DefaultTransactionDefinition()); // 1、job lock String lockedRecord = XxlJobAdminBootstrap.getInstance().getXxlJobLockMapper().scheduleLock(); long nowTime = System.currentTimeMillis(); // scan and process job List scheduleList = XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount); if (CollectionTool.isNotEmpty(scheduleList)) { // 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 // 1、misfire handle MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); misfireStrategyEnum.getMisfireHandler().handle(jobInfo.getId()); // 2、fresh next refreshNextTriggerTime(jobInfo, new Date()); } else if (nowTime > jobInfo.getTriggerNextTime()) { // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time // 1、trigger direct XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); logger.debug(">>>>>>>>>>> xxl-job, schedule expire, direct trigger : jobId = " + jobInfo.getId() ); // 2、fresh next refreshNextTriggerTime(jobInfo, new Date()); // next-trigger-time in 5s, pre-read again if (jobInfo.getTriggerStatus()== TriggerStatus.RUNNING.getValue() && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) { // 1、make ring second int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); // 2、push time ring (pre read) pushTimeRing(ringSecond, jobInfo.getId()); logger.debug(">>>>>>>>>>> xxl-job, schedule pre-read, push trigger : jobId = " + jobInfo.getId() ); // 3、fresh next refreshNextTriggerTime(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()); logger.debug(">>>>>>>>>>> xxl-job, schedule normal, push trigger : jobId = " + jobInfo.getId() ); // 3、fresh next refreshNextTriggerTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); } } // 3、update trigger info for (XxlJobInfo jobInfo: scheduleList) { XxlJobAdminBootstrap.getInstance().getXxlJobInfoMapper().scheduleUpdate(jobInfo); } } else { preReadSuc = false; } } catch (Throwable e) { if (!scheduleThreadToStop) { logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e.getMessage(), e); } } finally { // transaction commit try { if (transactionStatus != null) { XxlJobAdminBootstrap.getInstance().getTransactionManager().commit(transactionStatus); // avlid schedule repeat } } catch (Throwable e) { logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread transaction commit error:{}", e.getMessage(), e); } } // transaction end 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 (Throwable 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 (Throwable e) { if (!ringThreadToStop) { logger.error(e.getMessage(), e); } } try { // second data List ringItemData = new ArrayList<>(); // collect rind data, by second int nowSecond = Calendar.getInstance().get(Calendar.SECOND); for (int i = 0; i <= 2; i++) { // 避免调度遗漏:处理耗时太长、跨过刻度,除当前刻度外 + 向前校验2个刻度; List ringItemList = ringData.remove( (nowSecond+60-i)%60 ); if (CollectionTool.isNotEmpty(ringItemList)) { // distinct for each second List ringItemListDistinct = ringItemList.stream().distinct().toList(); // 避免调度重复:重复推送时间轮刻度,去重只保留一个;; if (ringItemListDistinct.size() < ringItemList.size()) { logger.warn(">>>>>>>>>>> xxl-job, time-ring found job repeat beat : " + nowSecond + " = " + ringItemData); } // collect ring item ringItemData.addAll(ringItemListDistinct); } } // ring trigger logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + ringItemData); if (CollectionTool.isNotEmpty(ringItemData)) { // do trigger for (int jobId: ringItemData) { // do trigger XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null); } // clear ringItemData.clear(); } } catch (Throwable e) { if (!ringThreadToStop) { logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e.getMessage(), e); } } } logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop"); } }); ringThread.setDaemon(true); ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread"); ringThread.start(); } /** * refresh next trigger time of job * * @param jobInfo job info * @param fromTime from time */ private void refreshNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) { try { // generate next trigger time ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), ScheduleTypeEnum.NONE); Date nextTriggerTime = scheduleTypeEnum.getScheduleType().generateNextTriggerTime(jobInfo, fromTime); // refresh next trigger-time + status if (nextTriggerTime != null) { // generate success jobInfo.setTriggerStatus(-1); // pass, may be Inaccurate jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime()); jobInfo.setTriggerNextTime(nextTriggerTime.getTime()); } else { // generate fail, stop job jobInfo.setTriggerStatus(TriggerStatus.STOPPED.getValue()); jobInfo.setTriggerLastTime(0); jobInfo.setTriggerNextTime(0); logger.error(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}", jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf()); } } catch (Throwable e) { // generate error, stop job jobInfo.setTriggerStatus(TriggerStatus.STOPPED.getValue()); jobInfo.setTriggerLastTime(0); jobInfo.setTriggerNextTime(0); logger.error(">>>>>>>>>>> xxl-job, refreshNextValidTime error for job: jobId={}, scheduleType={}, scheduleConf={}", jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf(), e); } } /** * push time ring * * @param ringSecond ring second * @param jobId job id */ private void pushTimeRing(int ringSecond, int jobId){ // get ringItemData, init when not exists List ringItemList = ringData.computeIfAbsent( ringSecond, k -> new ArrayList<>()); // push async rind ringItemList.add(jobId); logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + List.of(ringItemList)); } /** * stop */ public void stop(){ // 1、stop schedule scheduleThreadToStop = true; try { TimeUnit.SECONDS.sleep(1); // wait } catch (Throwable e) { logger.error(e.getMessage(), e); } if (scheduleThread.getState() != Thread.State.TERMINATED){ // interrupt and wait scheduleThread.interrupt(); try { scheduleThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } // if has ring data, wait for elegent shutdown boolean hasRingData = false; if (MapTool.isNotEmpty(ringData)) { for (int second : ringData.keySet()) { List ringItemList = ringData.get(second); if (CollectionTool.isNotEmpty(ringItemList)) { hasRingData = true; break; } } } if (hasRingData) { try { TimeUnit.SECONDS.sleep(ELEGANT_SHUTDOWN_WAITING_SECONDS); } catch (Throwable e) { logger.error(e.getMessage(), e); } } // stop ring (wait job-in-memory stop) ringThreadToStop = true; try { TimeUnit.SECONDS.sleep(1); } catch (Throwable e) { logger.error(e.getMessage(), e); } if (ringThread.getState() != Thread.State.TERMINATED){ // interrupt and wait ringThread.interrupt(); try { ringThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop"); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/thread/JobTriggerPoolHelper.java ================================================ package com.xxl.job.admin.scheduler.thread; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.trigger.TriggerTypeEnum; 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 final Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class); // ---------------------- trigger pool ---------------------- // fast/slow thread pool private ThreadPoolExecutor fastTriggerPool = null; private ThreadPoolExecutor slowTriggerPool = null; /** * start */ public void start(){ fastTriggerPool = new ThreadPoolExecutor( 10, XxlJobAdminBootstrap.getInstance().getTriggerPoolFastMax(), 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()); } }, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { logger.error(">>>>>>>>>>> xxl-job, admin JobTriggerPoolHelper-fastTriggerPool execute too fast, Runnable="+r.toString() ); } }); slowTriggerPool = new ThreadPoolExecutor( 10, XxlJobAdminBootstrap.getInstance().getTriggerPoolSlowMax(), 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(5000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()); } }, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { logger.error(">>>>>>>>>>> xxl-job, admin JobTriggerPoolHelper-slowTriggerPool execute too fast, Runnable="+r.toString() ); } }); } /** * stop */ 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 jobTimeoutCountMap = new ConcurrentHashMap<>(); // ---------------------- tool ---------------------- /** * 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 */ public void trigger(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 XxlJobAdminBootstrap.getInstance().getJobTrigger().trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); } catch (Throwable 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(); } } } } @Override public String toString() { return "Job Runnable, jobId:"+jobId; } }); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/trigger/JobTrigger.java ================================================ package com.xxl.job.admin.scheduler.trigger; import com.xxl.job.admin.mapper.XxlJobGroupMapper; import com.xxl.job.admin.mapper.XxlJobInfoMapper; import com.xxl.job.admin.mapper.XxlJobLogMapper; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLog; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.route.ExecutorRouteStrategyEnum; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.core.constant.ExecutorBlockStrategyEnum; import com.xxl.job.core.context.XxlJobContext; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.model.TriggerRequest; import com.xxl.tool.core.StringTool; import com.xxl.tool.error.ThrowableTool; import com.xxl.tool.http.IPTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Date; /** * xxl-job trigger * * @author xuxueli 17/7/13. */ @Component public class JobTrigger { private static final Logger logger = LoggerFactory.getLogger(JobTrigger.class); @Resource private XxlJobInfoMapper xxlJobInfoMapper; @Resource private XxlJobGroupMapper xxlJobGroupMapper; @Resource private XxlJobLogMapper xxlJobLogMapper; /** * trigger job * * @param jobId * @param triggerType * @param failRetryCount * >=0: use this param * <0: use param from job info config * @param executorShardingParam * null: new sharding, all nodes * not null: for retry, only one node * @param executorParam * null: use job param * not null: cover job param * @param addressList * null: use executor addressList * not null: cover */ public void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { // load data XxlJobInfo jobInfo = xxlJobInfoMapper.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 = xxlJobGroupMapper.load(jobInfo.getJobGroup()); // cover addressList if (StringTool.isNotBlank(addressList)) { group.setAddressType(1); group.setAddressList(addressList.trim()); } // sharding param int[] shardingParam = null; Date triggerTime = new Date(); if (executorShardingParam!=null){ String[] shardingArr = executorShardingParam.split("/"); if (shardingArr.length==2 && StringTool.isNumeric(shardingArr[0]) && StringTool.isNumeric(shardingArr[1])) { shardingParam = new int[2]; shardingParam[0] = Integer.parseInt(shardingArr[0]); shardingParam[1] = Integer.parseInt(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, triggerTime, i, group.getRegistryList().size()); } } else { if (shardingParam == null) { shardingParam = new int[]{0, 1}; } processTrigger(group, jobInfo, finalFailRetryCount, triggerType, triggerTime, shardingParam[0], shardingParam[1]); } } /*private static boolean isNumeric(String str){ try { int result = Integer.valueOf(str); return true; } catch (NumberFormatException e) { return false; } }*/ /** * process trigger with log * * @param group job group, registry list may be empty * @param jobInfo job info * @param finalFailRetryCount the fail-retry count * @param triggerType trigger type * @param triggerTime trigger time * @param index sharding index * @param total sharding index */ private void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, Date triggerTime, 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(triggerTime); xxlJobLogMapper.save(jobLog); logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId()); // 2、init trigger-param TriggerRequest triggerParam = new TriggerRequest(); 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; Response 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.isSuccess()) { address = routeAddressResult.getData(); } } } else { routeAddressResult = Response.of(XxlJobContext.HANDLE_CODE_FAIL, I18nUtil.getString("jobconf_trigger_address_empty")); } // 4、trigger remote executor Response triggerResult = null; if (address != null) { triggerResult = doTrigger(triggerParam, address); } else { triggerResult = Response.of(XxlJobContext.HANDLE_CODE_FAIL, "Address Router Fail."); } // 5、collection trigger info // trigger config StringBuilder triggerMsgSb = new StringBuilder(); triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle()); triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IPTool.getIp()); triggerMsgSb.append("
").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("
").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList()); triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()); if (shardingParam != null) { triggerMsgSb.append("(").append(shardingParam).append(")"); } triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle()); triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout()); triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount); // trigger data triggerMsgSb.append("

>>>>>>>>>>>").append(I18nUtil.getString("jobconf_trigger_run")).append("<<<<<<<<<<<
"); triggerMsgSb.append("
").append(I18nUtil.getString("joblog_field_executorAddress")).append(":"); if (StringTool.isNotBlank(address)) { triggerMsgSb.append(address); } else if (routeAddressResult!=null && !routeAddressResult.isSuccess() && routeAddressResult.getMsg()!=null) { triggerMsgSb.append("address route fail, ").append(routeAddressResult.getMsg()); } else { triggerMsgSb.append("address route fail."); } if (StringTool.isNotBlank(jobInfo.getExecutorHandler())) { triggerMsgSb.append("
").append("JobHandler").append(":").append(jobInfo.getExecutorHandler()); } triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorparam")).append(":").append(jobInfo.getExecutorParam()); triggerMsgSb.append("
").append(I18nUtil.getString("joblog_field_triggerMsg")).append(":"); if (triggerResult.isSuccess()) { triggerMsgSb.append("success"); } else if (triggerResult.getMsg()!=null) { triggerMsgSb.append("error, ").append(triggerResult.getMsg()); } else { triggerMsgSb.append("fail"); } // 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()); xxlJobLogMapper.updateTriggerInfo(jobLog); logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId()); } /** * do trigger with address * * @param triggerParam trigger param * @param address the address * @return return */ private Response doTrigger(TriggerRequest triggerParam, String address){ try { // build client ExecutorBiz executorBiz = XxlJobAdminBootstrap.getExecutorBiz(address); // invoke Response runResult = executorBiz.run(triggerParam); // build result StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":"); runResultSB.append("
address:").append(address); runResultSB.append("
code:").append(runResult.getCode()); runResultSB.append("
msg:").append(runResult.getMsg()); // return runResult.setMsg(runResultSB.toString()); return runResult; } catch (Exception e) { logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e); return Response.of(XxlJobContext.HANDLE_CODE_FAIL, ThrowableTool.toString(e)); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/trigger/TriggerTypeEnum.java ================================================ package com.xxl.job.admin.scheduler.trigger; import com.xxl.job.admin.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; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/type/ScheduleType.java ================================================ package com.xxl.job.admin.scheduler.type; import com.xxl.job.admin.model.XxlJobInfo; import java.util.Date; /** * Schedule Type * * @author xuxueli 2020-10-29 */ public abstract class ScheduleType { /** * generate next trigger time * * @param jobInfo job info * @param fromTime from time */ public abstract Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception; } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/type/ScheduleTypeEnum.java ================================================ package com.xxl.job.admin.scheduler.type; import com.xxl.job.admin.scheduler.type.strategy.CronScheduleType; import com.xxl.job.admin.scheduler.type.strategy.FixRateScheduleType; import com.xxl.job.admin.scheduler.type.strategy.NoneScheduleType; import com.xxl.job.admin.util.I18nUtil; /** * @author xuxueli 2020-10-29 21:11:23 */ public enum ScheduleTypeEnum { NONE(I18nUtil.getString("schedule_type_none"), new NoneScheduleType()), /** * schedule by cron */ CRON(I18nUtil.getString("schedule_type_cron"), new CronScheduleType()), /** * schedule by fixed rate (in seconds) */ FIX_RATE(I18nUtil.getString("schedule_type_fix_rate"), new FixRateScheduleType()), /** * schedule by fix delay (in seconds), after the last time */ /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/; private final String title; private final ScheduleType scheduleType;; ScheduleTypeEnum(String title, ScheduleType scheduleType) { this.title = title; this.scheduleType = scheduleType; } public String getTitle() { return title; } public ScheduleType getScheduleType() { return scheduleType; } /** * match by name * * @param name name of ScheduleTypeEnum * @param defaultItem default item * @return ScheduleTypeEnum */ public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){ for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) { if (item.name().equals(name)) { return item; } } return defaultItem; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/type/strategy/CronScheduleType.java ================================================ package com.xxl.job.admin.scheduler.type.strategy; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.scheduler.cron.CronExpression; import com.xxl.job.admin.scheduler.type.ScheduleType; import java.util.Date; public class CronScheduleType extends ScheduleType { @Override public Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { // generate next trigger time, with cron return new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/type/strategy/FixRateScheduleType.java ================================================ package com.xxl.job.admin.scheduler.type.strategy; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.scheduler.type.ScheduleType; import java.util.Date; public class FixRateScheduleType extends ScheduleType { @Override public Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { // generate next trigger time, fix rate delay return new Date(fromTime.getTime() + Long.parseLong(jobInfo.getScheduleConf()) * 1000L); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/type/strategy/NoneScheduleType.java ================================================ package com.xxl.job.admin.scheduler.type.strategy; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.scheduler.type.ScheduleType; import java.util.Date; public class NoneScheduleType extends ScheduleType { @Override public Date generateNextTriggerTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { // generate none trigger-time return null; } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java ================================================ package com.xxl.job.admin.service; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; 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 */ public Response> pageList(int offset, int pagesize, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author); /** * add job */ public Response add(XxlJobInfo jobInfo, LoginInfo loginInfo); /** * update job */ public Response update(XxlJobInfo jobInfo, LoginInfo loginInfo); /** * remove job */ public Response remove(int id, LoginInfo loginInfo); /** * start job */ public Response start(int id, LoginInfo loginInfo); /** * stop job */ public Response stop(int id, LoginInfo loginInfo); /** * trigger */ public Response trigger(LoginInfo loginInfo, int jobId, String executorParam, String addressList); /** * dashboard info */ public Map dashboardInfo(); /** * chart info */ public Response> chartInfo(Date startDate, Date endDate); } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java ================================================ package com.xxl.job.admin.service.impl; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.core.openapi.AdminBiz; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.openapi.model.RegistryRequest; import com.xxl.tool.response.Response; 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 Response callback(List callbackRequestList) { return XxlJobAdminBootstrap.getInstance().getJobCompleteHelper().callback(callbackRequestList); } @Override public Response registry(RegistryRequest registryRequest) { return XxlJobAdminBootstrap.getInstance().getJobRegistryHelper().registry(registryRequest); } @Override public Response registryRemove(RegistryRequest registryRequest) { return XxlJobAdminBootstrap.getInstance().getJobRegistryHelper().registryRemove(registryRequest); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java ================================================ package com.xxl.job.admin.service.impl; import com.xxl.job.admin.constant.TriggerStatus; import com.xxl.job.admin.mapper.*; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.model.XxlJobLogReport; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.job.admin.scheduler.cron.CronExpression; import com.xxl.job.admin.scheduler.misfire.MisfireStrategyEnum; import com.xxl.job.admin.scheduler.route.ExecutorRouteStrategyEnum; import com.xxl.job.admin.scheduler.thread.JobScheduleHelper; import com.xxl.job.admin.scheduler.trigger.TriggerTypeEnum; import com.xxl.job.admin.scheduler.type.ScheduleTypeEnum; import com.xxl.job.admin.service.XxlJobService; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.admin.util.JobGroupPermissionUtil; import com.xxl.job.core.constant.ExecutorBlockStrategyEnum; import com.xxl.job.core.glue.GlueTypeEnum; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.DateTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; 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 XxlJobGroupMapper xxlJobGroupMapper; @Resource private XxlJobInfoMapper xxlJobInfoMapper; @Resource public XxlJobLogMapper xxlJobLogMapper; @Resource private XxlJobLogGlueMapper xxlJobLogGlueMapper; @Resource private XxlJobLogReportMapper xxlJobLogReportMapper; @Override public Response> pageList(int offset, int pagesize, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { // page list List list = xxlJobInfoMapper.pageList(offset, pagesize, jobGroup, triggerStatus, jobDesc, executorHandler, author); int list_count = xxlJobInfoMapper.pageListCount(offset, pagesize, jobGroup, triggerStatus, jobDesc, executorHandler, author); // package result PageModel pageModel = new PageModel<>(); pageModel.setData(list); pageModel.setTotal(list_count); return Response.ofSuccess(pageModel); } @Override public Response add(XxlJobInfo jobInfo, LoginInfo loginInfo) { // valid base XxlJobGroup group = xxlJobGroupMapper.load(jobInfo.getJobGroup()); if (group == null) { return Response.ofFail (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")); } if (StringTool.isBlank(jobInfo.getJobDesc())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); } if (StringTool.isBlank(jobInfo.getAuthor())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); } // valid trigger ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); if (scheduleTypeEnum == null) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { return Response.ofFail ( "Cron"+I18nUtil.getString("system_unvalid")); } } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { if (jobInfo.getScheduleConf() == null) { return Response.ofFail ( (I18nUtil.getString("schedule_type")) ); } try { int fixSecond = Integer.parseInt(jobInfo.getScheduleConf()); if (fixSecond < 1) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } } catch (Exception e) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } } // valid job if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) ); } if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && StringTool.isBlank(jobInfo.getExecutorHandler()) ) { return Response.ofFail ( (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 Response.ofFail ( (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); } if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { return Response.ofFail ( (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); } if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); } // 》ChildJobId valid if (StringTool.isNotBlank(jobInfo.getChildJobId())) { String[] childJobIds = jobInfo.getChildJobId().split(","); for (String childJobIdItem: childJobIds) { if (StringTool.isNotBlank(childJobIdItem) && StringTool.isNumeric(childJobIdItem)) { XxlJobInfo childJobInfo = xxlJobInfoMapper.loadById(Integer.parseInt(childJobIdItem)); if (childJobInfo==null) { return Response.ofFail ( MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); } // valid jobGroup permission if (!JobGroupPermissionUtil.hasJobGroupPermission(loginInfo, childJobInfo.getJobGroup())) { return Response.ofFail ( MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_permission_limit")), childJobIdItem)); } } else { return Response.ofFail ( 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()); // remove the whitespace jobInfo.setExecutorHandler(jobInfo.getExecutorHandler().trim()); xxlJobInfoMapper.save(jobInfo); if (jobInfo.getId() < 1) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) ); } // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobinfo-save", GsonTool.toJson(jobInfo)); return Response.ofSuccess(String.valueOf(jobInfo.getId())); } @Override public Response update(XxlJobInfo jobInfo, LoginInfo loginInfo) { // valid base if (StringTool.isBlank(jobInfo.getJobDesc())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); } if (StringTool.isBlank(jobInfo.getAuthor())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); } // valid trigger ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); if (scheduleTypeEnum == null) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { return Response.ofFail ( "Cron"+I18nUtil.getString("system_unvalid") ); } } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE /*|| scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { if (jobInfo.getScheduleConf() == null) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } try { int fixSecond = Integer.parseInt(jobInfo.getScheduleConf()); if (fixSecond < 1) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } } catch (Exception e) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } } // valid advanced if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); } if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { return Response.ofFail ( (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); } if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); } // 》ChildJobId valid if (StringTool.isNotBlank(jobInfo.getChildJobId())) { String[] childJobIds = jobInfo.getChildJobId().split(","); for (String childJobIdItem: childJobIds) { if (StringTool.isNotBlank(childJobIdItem) && StringTool.isNumeric(childJobIdItem)) { // parse child int childJobId = Integer.parseInt(childJobIdItem); if (childJobId == jobInfo.getId()) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_childJobId")+"("+childJobId+")"+I18nUtil.getString("system_unvalid")) ); } // valid child XxlJobInfo childJobInfo = xxlJobInfoMapper.loadById(childJobId); if (childJobInfo==null) { return Response.ofFail ( MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); } // valid jobGroup permission if (!JobGroupPermissionUtil.hasJobGroupPermission(loginInfo, childJobInfo.getJobGroup())) { return Response.ofFail ( MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_permission_limit")), childJobIdItem)); } } else { return Response.ofFail ( 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 = xxlJobGroupMapper.load(jobInfo.getJobGroup()); if (jobGroup == null) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_jobgroup")+I18nUtil.getString("system_unvalid")) ); } // stage job info XxlJobInfo exists_jobInfo = xxlJobInfoMapper.loadById(jobInfo.getId()); if (exists_jobInfo == null) { return Response.ofFail ( (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() == TriggerStatus.RUNNING.getValue() && !scheduleDataNotChanged) { try { // generate next trigger time Date nextValidTime = scheduleTypeEnum.getScheduleType().generateNextTriggerTime(jobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); if (nextValidTime == null) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } nextTriggerTime = nextValidTime.getTime(); } catch (Exception e) { logger.error(e.getMessage(), e); return Response.ofFail ( (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()); // remove the whitespace exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler().trim()); 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()); xxlJobInfoMapper.update(exists_jobInfo); // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobinfo-update", GsonTool.toJson(exists_jobInfo)); return Response.ofSuccess(); } @Override public Response remove(int id, LoginInfo loginInfo) { // valid job XxlJobInfo xxlJobInfo = xxlJobInfoMapper.loadById(id); if (xxlJobInfo == null) { return Response.ofSuccess(); } // valid jobGroup permission if (!JobGroupPermissionUtil.hasJobGroupPermission(loginInfo, xxlJobInfo.getJobGroup())) { return Response.ofFail(I18nUtil.getString("system_permission_limit")); } xxlJobInfoMapper.delete(id); xxlJobLogMapper.delete(id); xxlJobLogGlueMapper.deleteByJobId(id); // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobinfo-remove", id); return Response.ofSuccess(); } @Override public Response start(int id, LoginInfo loginInfo) { // load and valid XxlJobInfo xxlJobInfo = xxlJobInfoMapper.loadById(id); if (xxlJobInfo == null) { return Response.ofFail(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); } // valid jobGroup permission if (!JobGroupPermissionUtil.hasJobGroupPermission(loginInfo, xxlJobInfo.getJobGroup())) { return Response.ofFail(I18nUtil.getString("system_permission_limit")); } // valid ScheduleType: can not be none ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE); if (ScheduleTypeEnum.NONE == scheduleTypeEnum) { return Response.ofFail(I18nUtil.getString("schedule_type_none_limit_start")); } // next trigger time (5s后生效,避开预读周期) long nextTriggerTime = 0; try { // generate next trigger time Date nextValidTime = scheduleTypeEnum.getScheduleType().generateNextTriggerTime(xxlJobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); if (nextValidTime == null) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } nextTriggerTime = nextValidTime.getTime(); } catch (Exception e) { logger.error(e.getMessage(), e); return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } xxlJobInfo.setTriggerStatus(TriggerStatus.RUNNING.getValue()); xxlJobInfo.setTriggerLastTime(0); xxlJobInfo.setTriggerNextTime(nextTriggerTime); xxlJobInfo.setUpdateTime(new Date()); xxlJobInfoMapper.update(xxlJobInfo); // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobinfo-start", id); return Response.ofSuccess(); } @Override public Response stop(int id, LoginInfo loginInfo) { // load and valid XxlJobInfo xxlJobInfo = xxlJobInfoMapper.loadById(id); if (xxlJobInfo == null) { return Response.ofFail(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); } // valid jobGroup permission if (!JobGroupPermissionUtil.hasJobGroupPermission(loginInfo, xxlJobInfo.getJobGroup())) { return Response.ofFail(I18nUtil.getString("system_permission_limit")); } // stop xxlJobInfo.setTriggerStatus(TriggerStatus.STOPPED.getValue()); xxlJobInfo.setTriggerLastTime(0); xxlJobInfo.setTriggerNextTime(0); xxlJobInfo.setUpdateTime(new Date()); xxlJobInfoMapper.update(xxlJobInfo); // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobinfo-stop", id); return Response.ofSuccess(); } @Override public Response trigger(LoginInfo loginInfo, int jobId, String executorParam, String addressList) { // valid job XxlJobInfo xxlJobInfo = xxlJobInfoMapper.loadById(jobId); if (xxlJobInfo == null) { return Response.ofFail(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); } // valid jobGroup permission if (!JobGroupPermissionUtil.hasJobGroupPermission(loginInfo, xxlJobInfo.getJobGroup())) { return Response.ofFail(I18nUtil.getString("system_permission_limit")); } // force cover job param if (executorParam == null) { executorParam = ""; } XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); // write operation log logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", loginInfo.getUserName(), "jobinfo-trigger", jobId); return Response.ofSuccess(); } @Override public Map dashboardInfo() { int jobInfoCount = xxlJobInfoMapper.findAllCount(); int jobLogCount = 0; int jobLogSuccessCount = 0; XxlJobLogReport xxlJobLogReport = xxlJobLogReportMapper.queryLogReportTotal(); if (xxlJobLogReport != null) { jobLogCount = xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount(); jobLogSuccessCount = xxlJobLogReport.getSucCount(); } // executor count Set executorAddressSet = new HashSet(); List groupList = xxlJobGroupMapper.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 dashboardMap = new HashMap(); dashboardMap.put("jobInfoCount", jobInfoCount); dashboardMap.put("jobLogCount", jobLogCount); dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount); dashboardMap.put("executorCount", executorCount); return dashboardMap; } @Override public Response> chartInfo(Date startDate, Date endDate) { // process List triggerDayList = new ArrayList(); List triggerDayCountRunningList = new ArrayList(); List triggerDayCountSucList = new ArrayList(); List triggerDayCountFailList = new ArrayList(); int triggerCountRunningTotal = 0; int triggerCountSucTotal = 0; int triggerCountFailTotal = 0; List logReportList = xxlJobLogReportMapper.queryLogReport(startDate, endDate); if (logReportList!=null && !logReportList.isEmpty()) { for (XxlJobLogReport item: logReportList) { String day = DateTool.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(DateTool.formatDate(DateTool.addDays(new Date(), i))); triggerDayCountRunningList.add(0); triggerDayCountSucList.add(0); triggerDayCountFailList.add(0); } } Map result = new HashMap(); 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 Response.ofSuccess(result); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/I18nUtil.java ================================================ package com.xxl.job.admin.util; import com.xxl.job.core.constant.ExecutorBlockStrategyEnum; import com.xxl.tool.core.PropTool; import com.xxl.tool.freemarker.FtlTool; import com.xxl.tool.json.GsonTool; import freemarker.template.Configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * i18n util * * @author xuxueli 2018-01-17 20:39:06 */ @Component public class I18nUtil implements InitializingBean { private static Logger logger = LoggerFactory.getLogger(I18nUtil.class); // ---------------------- for i18n config ---------------------- /** * i18n config */ @Value("${xxl.job.i18n}") private String i18n; /** * freemarker config */ @Autowired private Configuration configuration; @Override public void afterPropertiesSet() throws Exception { // init freemarker shared variable configuration.setSharedVariable("I18nUtil", FtlTool.generateStaticModel(I18nUtil.class.getName())); // init single single = this; // init i18n-enum initI18nEnum(); } /** * get i18n */ public String getI18n() { if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) { return "zh_CN"; } return i18n; } private static I18nUtil single = null; private static I18nUtil getSingle() { return single; } // ---------------------- tool ---------------------- private static Properties prop = null; public static Properties loadI18nProp(){ if (prop != null) { return prop; } // build i18n filepath String i18n = getSingle().getI18n(); String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n); // load prop prop = PropTool.loadProp(i18nFile); 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 map = new HashMap<>(); 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)); } } return GsonTool.toJson(map); } // ---------------------- init I18n-enum ---------------------- /** * init i18n-enum */ private void initI18nEnum(){ for (ExecutorBlockStrategyEnum item : ExecutorBlockStrategyEnum.values()) { item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/JobGroupPermissionUtil.java ================================================ package com.xxl.job.admin.util; import com.xxl.job.admin.constant.Consts; import com.xxl.job.admin.model.XxlJobGroup; import com.xxl.sso.core.helper.XxlSsoHelper; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.StringTool; import com.xxl.tool.response.Response; import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; /** * jobGroup permission util * * @author xuxueli 2025-08-24 */ public class JobGroupPermissionUtil { /** * check if has jobgroup permission */ public static boolean hasJobGroupPermission(LoginInfo loginInfo, int jobGroup){ if (XxlSsoHelper.hasRole(loginInfo, Consts.ADMIN_ROLE).isSuccess()) { return true; } else { List jobGroups = (loginInfo.getExtraInfo()!=null && loginInfo.getExtraInfo().containsKey("jobGroups")) ? StringTool.split(loginInfo.getExtraInfo().get("jobGroups"), ",") :new ArrayList<>(); return jobGroups.contains(String.valueOf(jobGroup)); } } /** * valid jobGroup permission */ public static LoginInfo validJobGroupPermission(HttpServletRequest request, int jobGroup) { Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); if (!(loginInfoResponse.isSuccess() && hasJobGroupPermission(loginInfoResponse.getData(), jobGroup))) { throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginInfoResponse.getData().getUserName() +"]"); } return loginInfoResponse.getData(); } /** * filter jobGroupList by permission */ public static List filterJobGroupByPermission(HttpServletRequest request, List jobGroupListTotal){ Response loginInfoResponse = XxlSsoHelper.loginCheckWithAttr(request); if (XxlSsoHelper.hasRole(loginInfoResponse.getData(), Consts.ADMIN_ROLE).isSuccess()) { return jobGroupListTotal; } else { List jobGroups = (loginInfoResponse.getData().getExtraInfo()!=null && loginInfoResponse.getData().getExtraInfo().get("jobGroups")!=null ) ? StringTool.split(loginInfoResponse.getData().getExtraInfo().get("jobGroups"), ",") :new ArrayList<>(); return jobGroupListTotal .stream() .filter(jobGroup -> jobGroups.contains(String.valueOf(jobGroup.getId()))) .toList(); } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/CommonDataInterceptor.java ================================================ //package com.xxl.job.admin.web.interceptor; // //import com.xxl.job.admin.util.I18nUtil; //import com.xxl.tool.freemarker.FtlTool; //import jakarta.servlet.http.HttpServletRequest; //import jakarta.servlet.http.HttpServletResponse; //import org.springframework.context.annotation.Configuration; //import org.springframework.web.servlet.AsyncHandlerInterceptor; //import org.springframework.web.servlet.ModelAndView; //import org.springframework.web.servlet.config.annotation.InterceptorRegistry; //import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // ///** // * push cookies to model as cookieMap // * // * @author xuxueli 2015-12-12 18:09:04 // */ //@Configuration //public class CommonDataInterceptor implements WebMvcConfigurer { // // @Override // public void addInterceptors(InterceptorRegistry registry) { // registry.addInterceptor(new AsyncHandlerInterceptor() { // @Override // public void postHandle(HttpServletRequest request, // HttpServletResponse response, // Object handler, // ModelAndView modelAndView) throws Exception { // // // static method // if (modelAndView != null) { // modelAndView.addObject("I18nUtil", FtlTool.generateStaticModel(I18nUtil.class.getName())); // } // // } // }).addPathPatterns("/**"); // } // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/CookieUtil.java ================================================ //package com.xxl.job.admin.util; // //import jakarta.servlet.http.Cookie; //import jakarta.servlet.http.HttpServletRequest; //import jakarta.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); // } // } // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/FtlUtil.java ================================================ //package com.xxl.job.admin.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; // } // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/JacksonUtil.java ================================================ //package com.xxl.job.admin.util.old; // //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 readValue(String jsonStr, Class 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... // * // * @param jsonStr // * @param parametrized // * @param parameterClasses // * @param // * @return // */ // public static 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; // } //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/LocalCacheUtil.java ================================================ //package com.xxl.job.admin.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 cacheRepository = new ConcurrentHashMap(); // 类型建议用抽象父类,兼容性更好; // 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()) { // cacheRepository.remove(key); // } // } // } // return true; // } // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/RemoteHttpJobBean.java ================================================ //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()); // // // } // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/XxlJobDynamicScheduler.java ================================================ //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 executorBizRepository = new ConcurrentHashMap(); // 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 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 triggerSet = new HashSet(); // 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> finaAllJobList(){ // List> jobList = new ArrayList>(); // // try { // if (scheduler.getJobGroupNames()==null || scheduler.getJobGroupNames().size()==0) { // return null; // } // String groupName = scheduler.getJobGroupNames().get(0); // Set 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 jobMap = new HashMap(); // 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; // }*/ // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/util/old/XxlJobThreadPool.java ================================================ //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) { // // // } // //} ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/web/error/WebErrorPageRegistrar.java ================================================ package com.xxl.job.admin.web.error; import org.springframework.boot.web.error.ErrorPage; import org.springframework.boot.web.error.ErrorPageRegistrar; import org.springframework.boot.web.error.ErrorPageRegistry; import org.springframework.stereotype.Component; /** * error page */ @Component public class WebErrorPageRegistrar implements ErrorPageRegistrar { @Override public void registerErrorPages(ErrorPageRegistry registry) { ErrorPage errorPage = new ErrorPage("/errorpage"); registry.addErrorPages(errorPage); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/web/error/WebHandlerExceptionResolver.java ================================================ package com.xxl.job.admin.web.error; import com.xxl.job.admin.scheduler.exception.XxlJobException; import com.xxl.tool.json.GsonTool; import com.xxl.tool.response.Response; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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 java.io.IOException; /** * common exception resolver * * @author xuxueli 2016-1-6 19:22:18 */ @Component public class WebHandlerExceptionResolver implements HandlerExceptionResolver { private static transient Logger logger = LoggerFactory.getLogger(WebHandlerExceptionResolver.class); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (!(ex instanceof XxlJobException)) { logger.error("WebExceptionResolver:{}", ex); } // parse isJson boolean isJson = false; if (handler instanceof HandlerMethod) { HandlerMethod method = (HandlerMethod)handler; isJson = method.getMethodAnnotation(ResponseBody.class)!=null; } // process error ModelAndView mv = new ModelAndView(); if (isJson) { try { // errorMsg String errorMsg = GsonTool.toJson(Response.ofFail(ex.toString())); // write response response.setStatus(HttpServletResponse.SC_OK); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(errorMsg); } catch (IOException e) { logger.error(e.getMessage(), e); } return mv; } else { mv.addObject("exceptionMsg", ex.toString()); mv.setViewName("common/common.errorpage"); return mv; } } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/web/xxlsso/SimpleLoginStore.java ================================================ package com.xxl.job.admin.web.xxlsso; import com.xxl.job.admin.constant.Consts; import com.xxl.job.admin.mapper.XxlJobUserMapper; import com.xxl.job.admin.model.XxlJobUser; import com.xxl.sso.core.model.LoginInfo; import com.xxl.sso.core.store.LoginStore; import com.xxl.tool.core.MapTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; /** * Simple LoginStore * * 1、store by database; * 2、If you have higher performance requirements, it is recommended to use RedisLoginStore; * * @author xuxueli 2025-08-03 */ @Component public class SimpleLoginStore implements LoginStore { @Resource private XxlJobUserMapper xxlJobUserMapper; @Override public Response set(LoginInfo loginInfo) { // parse token-signature String token_sign = loginInfo.getSignature(); // write token by UserId int ret = xxlJobUserMapper.updateToken(Integer.parseInt(loginInfo.getUserId()), token_sign); return ret > 0 ? Response.ofSuccess() : Response.ofFail("token set fail"); } @Override public Response update(LoginInfo loginInfo) { return Response.ofFail("not support"); } @Override public Response remove(String userId) { // delete token-signature int ret = xxlJobUserMapper.updateToken(Integer.parseInt(userId), ""); return ret > 0 ? Response.ofSuccess() : Response.ofFail("token remove fail"); } /** * check through DB query */ @Override public Response get(String userId) { // load login-user XxlJobUser user = xxlJobUserMapper.loadById(Integer.parseInt(userId)); if (user == null) { return Response.ofFail("userId invalid."); } // parse role List roleList = user.getRole()==1? List.of(Consts.ADMIN_ROLE):null; // parse jobGroup permission Map extraInfo = MapTool.newMap( "jobGroups", user.getPermission() ); // build LoginInfo LoginInfo loginInfo = new LoginInfo(userId, user.getToken()); loginInfo.setUserName(user.getUsername()); loginInfo.setRoleList(roleList); loginInfo.setExtraInfo(extraInfo); return Response.ofSuccess(loginInfo); } } ================================================ FILE: xxl-job-admin/src/main/java/com/xxl/job/admin/web/xxlsso/XxlSsoConfig.java ================================================ package com.xxl.job.admin.web.xxlsso; import com.xxl.sso.core.auth.interceptor.XxlSsoWebInterceptor; import com.xxl.sso.core.bootstrap.XxlSsoBootstrap; import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author xuxueli 2018-11-15 */ @Configuration public class XxlSsoConfig implements WebMvcConfigurer { @Value("${xxl-sso.token.key}") private String tokenKey; @Value("${xxl-sso.token.timeout}") private long tokenTimeout; @Value("${xxl-sso.client.excluded.paths}") private String excludedPaths; @Value("${xxl-sso.client.login.path}") private String loginPath; @Resource private SimpleLoginStore loginStore; /** * 1、配置 XxlSsoBootstrap */ @Bean(initMethod = "start", destroyMethod = "stop") public XxlSsoBootstrap xxlSsoBootstrap() { XxlSsoBootstrap bootstrap = new XxlSsoBootstrap(); bootstrap.setLoginStore(loginStore); bootstrap.setTokenKey(tokenKey); bootstrap.setTokenTimeout(tokenTimeout); return bootstrap; } /** * 2、配置 XxlSso 拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 2.1、build xxl-sso interceptor XxlSsoWebInterceptor webInterceptor = new XxlSsoWebInterceptor(excludedPaths, loginPath); // 2.2、add interceptor registry.addInterceptor(webInterceptor).addPathPatterns("/**"); } } ================================================ FILE: xxl-job-admin/src/main/resources/application.properties ================================================ ### web server.port=8080 server.servlet.context-path=/xxl-job-admin ### actuator management.endpoints.web.base-path=/actuator management.health.mail.enabled=false ### resources spring.mvc.servlet.load-on-startup=0 spring.mvc.static-path-pattern=/static/** spring.web.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.########## spring.freemarker.settings.new_builtin_class_resolver=safer ### mybatis mybatis.mapper-locations=classpath:/mapper/*Mapper.xml ### 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, datasource 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.cj.jdbc.Driver ### 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, timeout by second, default 3s xxl.job.timeout=3 ### 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=300 xxl.job.triggerpool.slow.max=200 ### xxl-job, log retention days xxl.job.logretentiondays=30 ### xxl-sso xxl-sso.token.key=xxl_job_login_token xxl-sso.token.timeout=604800000 xxl-sso.client.excluded.paths= xxl-sso.client.login.path=/auth/login ================================================ FILE: xxl-job-admin/src/main/resources/i18n/message_en.properties ================================================ admin_name=Scheduling Center admin_name_full=Distributed Task Scheduling Platform|XXL-JOB admin_version=3.4.0-SNAPSHOT admin_i18n=en ## system system_tips=System message system_ok=Confirm system_close=Close system_save=Save system_cancel=Cancel system_search=Search system_reset=Reset system_status=Status system_opt=Operate system_opt_add=Add system_please_input=please input system_please_choose=please choose system_success=success system_fail=fail system_error=error system_all=All 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 system_num_range=Numerical range limit system_one=One system_data=Data system_selected_nothing=No data selected ## tab tab_opt=Tab Operation tab_close_current=Close Current Tabs tab_close_other=Close Other Tabs tab_close_all=Close All Tabs tab_refresh=Refresh Tab ## daterangepicker 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 ## 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_oldpwd=old password change_pwd_field_newpwd=new password ## change skin change_skin=Change Theme ## help admin_help=User Guide admin_help_document=Official Documentation ## 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_source=GLUE Source 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}
jobconf_callback_child_msg2={0}/{1} [Job ID={2}], Trigger Fail, Trigger msg: Job ID is illegal
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 ================================================ FILE: xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties ================================================ admin_name=任务调度中心 admin_name_full=分布式任务调度平台|XXL-JOB admin_version=3.4.0-SNAPSHOT admin_i18n= ## system system_tips=系统提示 system_ok=确定 system_close=关闭 system_save=保存 system_cancel=取消 system_search=搜索 system_reset=重置 system_status=状态 system_opt=操作 system_opt_add=新增 system_please_input=请输入 system_please_choose=请选择 system_success=成功 system_fail=失败 system_error=错误 system_all=全部 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=欢迎 system_num_range=数值范围限制 system_one=一条 system_data=数据 system_selected_nothing=未选择 ## tab tab_opt=页签操作 tab_close_current=关闭当前 tab_close_other=关闭其他 tab_close_all=全部关闭 tab_refresh=刷新 ## daterangepicker 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=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 ## 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_oldpwd=旧密码 change_pwd_field_newpwd=新密码 ## change skin change_skin=切换主题 ## help admin_help=使用教程 admin_help_document=官方文档 ## 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_source=GLUE源码 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}
jobconf_callback_child_msg2={0}/{1} [任务ID={2}], 触发失败, 触发备注: 任务ID格式错误
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=禁止操作当前登录账号 ================================================ FILE: xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties ================================================ admin_name=任務調度中心 admin_name_full=分布式任務調度平臺|XXL-JOB admin_version=3.4.0-SNAPSHOT admin_i18n= ## system system_tips=系統提示 system_ok=確定 system_close=關閉 system_save=儲存 system_cancel=取消 system_search=搜尋 system_reset=重置 system_status=狀態 system_opt=操作 system_opt_add=新增 system_please_input=請輸入 system_please_choose=请選擇 system_success=成功 system_fail=失敗 system_error=錯誤 system_all=全部 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=歡迎 system_num_range=數值範圍限制 system_one=一條 system_data=數據 system_selected_nothing=请選擇 ## tab tab_opt=頁籤操作 tab_close_current=關閉當前 tab_close_other=關閉其他 tab_close_all=全部關閉 tab_refresh=刷新 ## daterangepicker 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=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 ## 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_oldpwd=舊密碼 change_pwd_field_newpwd=新密碼 ## change skin change_skin=切換主題 ## help admin_help=使用教程 admin_help_document=官方文檔 ## 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_source=GLUE源碼 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}
jobconf_callback_child_msg2={0}/{1} [任務ID={2}], 觸發失败, 觸發備註: 任務ID格式錯誤
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=禁止操作當前登入帳號 ================================================ FILE: xxl-job-admin/src/main/resources/logback.xml ================================================ logback %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ${log.path} ${log.path}.%d{yyyy-MM-dd}.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobGroupMapper.xml ================================================ t.id, t.app_name, t.title, t.address_type, t.address_list, t.update_time INSERT INTO xxl_job_group ( `app_name`, `title`, `address_type`, `address_list`, `update_time`) values ( #{appname}, #{title}, #{addressType}, #{addressList}, #{updateTime} ); UPDATE xxl_job_group SET `app_name` = #{appname}, `title` = #{title}, `address_type` = #{addressType}, `address_list` = #{addressList}, `update_time` = #{updateTime} WHERE id = #{id} DELETE FROM xxl_job_group WHERE id = #{id} ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobInfoMapper.xml ================================================ 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 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} ); 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} DELETE FROM xxl_job_info WHERE id = #{id} UPDATE xxl_job_info SET trigger_last_time = #{triggerLastTime}, trigger_next_time = #{triggerNextTime} , trigger_status = #{triggerStatus} WHERE id = #{id} AND trigger_status = 1 ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobLockMapper.xml ================================================ ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobLogGlueMapper.xml ================================================ t.id, t.job_id, t.glue_type, t.glue_source, t.glue_remark, t.add_time, t.update_time INSERT INTO xxl_job_logglue ( `job_id`, `glue_type`, `glue_source`, `glue_remark`, `add_time`, `update_time` ) VALUES ( #{jobId}, #{glueType}, #{glueSource}, #{glueRemark}, #{addTime}, #{updateTime} ); 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 FROM xxl_job_logglue WHERE `job_id` = #{jobId} ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobLogMapper.xml ================================================ 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 INSERT INTO xxl_job_log ( `job_group`, `job_id`, `trigger_time`, `trigger_code`, `handle_code` ) VALUES ( #{jobGroup}, #{jobId}, #{triggerTime}, #{triggerCode}, #{handleCode} ); 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 xxl_job_log SET `handle_time`= #{handleTime}, `handle_code`= #{handleCode}, `handle_msg`= #{handleMsg} WHERE `id`= #{id} delete from xxl_job_log WHERE job_id = #{jobId} delete from xxl_job_log WHERE id in #{item} UPDATE xxl_job_log SET `alarm_status` = #{newAlarmStatus} WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus} ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobLogReportMapper.xml ================================================ t.id, t.trigger_day, t.running_count, t.suc_count, t.fail_count INSERT INTO xxl_job_log_report ( `trigger_day`, `running_count`, `suc_count`, `fail_count` ) VALUES ( #{triggerDay}, #{runningCount}, #{sucCount}, #{failCount} ) ON DUPLICATE KEY UPDATE `running_count` = #{runningCount}, `suc_count` = #{sucCount}, `fail_count` = #{failCount} ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobRegistryMapper.xml ================================================ t.id, t.registry_group, t.registry_key, t.registry_value, t.update_time DELETE FROM xxl_job_registry WHERE id in #{item} INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`) VALUES( #{registryGroup} , #{registryKey} , #{registryValue}, #{updateTime}) ON DUPLICATE KEY UPDATE `update_time` = #{updateTime} DELETE FROM xxl_job_registry WHERE registry_group = #{registryGroup} AND registry_key = #{registryKey} AND registry_value = #{registryValue} DELETE FROM xxl_job_registry WHERE registry_group = #{registryGroup} AND registry_key = #{registryKey} ================================================ FILE: xxl-job-admin/src/main/resources/mapper/XxlJobUserMapper.xml ================================================ t.id, t.username, t.password, t.token, t.role, t.permission INSERT INTO xxl_job_user ( username, password, role, permission ) VALUES ( #{username}, #{password}, #{role}, #{permission} ); UPDATE xxl_job_user SET password = #{password}, role = #{role}, permission = #{permission} WHERE id = #{id} DELETE FROM xxl_job_user WHERE id = #{id} UPDATE xxl_job_user SET token = #{token} WHERE id = #{id} ================================================ FILE: xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css ================================================ .daterangepicker { position: absolute; color: inherit; background-color: #fff; border-radius: 4px; width: 278px; padding: 4px; margin-top: 1px; top: 100px; left: 20px; /* Calendars */ } .daterangepicker:before, .daterangepicker:after { position: absolute; display: inline-block; border-bottom-color: rgba(0, 0, 0, 0.2); content: ''; } .daterangepicker:before { top: -7px; border-right: 7px solid transparent; border-left: 7px solid transparent; border-bottom: 7px solid #ccc; } .daterangepicker:after { top: -6px; border-right: 6px solid transparent; border-bottom: 6px solid #fff; border-left: 6px solid transparent; } .daterangepicker.opensleft:before { right: 9px; } .daterangepicker.opensleft:after { right: 10px; } .daterangepicker.openscenter:before { left: 0; right: 0; width: 0; margin-left: auto; margin-right: auto; } .daterangepicker.openscenter:after { left: 0; right: 0; width: 0; margin-left: auto; margin-right: auto; } .daterangepicker.opensright:before { left: 9px; } .daterangepicker.opensright:after { left: 10px; } .daterangepicker.dropup { margin-top: -5px; } .daterangepicker.dropup:before { top: initial; bottom: -7px; border-bottom: initial; border-top: 7px solid #ccc; } .daterangepicker.dropup:after { top: initial; bottom: -6px; border-bottom: initial; border-top: 6px solid #fff; } .daterangepicker.dropdown-menu { max-width: none; z-index: 3001; } .daterangepicker.single .ranges, .daterangepicker.single .calendar { float: none; } .daterangepicker.show-calendar .calendar { display: block; } .daterangepicker .calendar { display: none; max-width: 270px; margin: 4px; } .daterangepicker .calendar.single .calendar-table { border: none; } .daterangepicker .calendar th, .daterangepicker .calendar td { white-space: nowrap; text-align: center; min-width: 32px; } .daterangepicker .calendar-table { border: 1px solid #fff; padding: 4px; border-radius: 4px; background-color: #fff; } .daterangepicker table { width: 100%; margin: 0; } .daterangepicker td, .daterangepicker th { text-align: center; width: 20px; height: 20px; border-radius: 4px; border: 1px solid transparent; white-space: nowrap; cursor: pointer; } .daterangepicker td.available:hover, .daterangepicker th.available:hover { background-color: #eee; border-color: transparent; color: inherit; } .daterangepicker td.week, .daterangepicker th.week { font-size: 80%; color: #ccc; } .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { background-color: #fff; border-color: transparent; color: #999; } .daterangepicker td.in-range { background-color: #ebf4f8; border-color: transparent; color: #000; border-radius: 0; } .daterangepicker td.start-date { border-radius: 4px 0 0 4px; } .daterangepicker td.end-date { border-radius: 0 4px 4px 0; } .daterangepicker td.start-date.end-date { border-radius: 4px; } .daterangepicker td.active, .daterangepicker td.active:hover { background-color: #357ebd; border-color: transparent; color: #fff; } .daterangepicker th.month { width: auto; } .daterangepicker td.disabled, .daterangepicker option.disabled { color: #999; cursor: not-allowed; text-decoration: line-through; } .daterangepicker select.monthselect, .daterangepicker select.yearselect { font-size: 12px; padding: 1px; height: auto; margin: 0; cursor: default; } .daterangepicker select.monthselect { margin-right: 2%; width: 56%; } .daterangepicker select.yearselect { width: 40%; } .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { width: 50px; margin-bottom: 0; } .daterangepicker .input-mini { border: 1px solid #ccc; border-radius: 4px; color: #555; height: 30px; line-height: 30px; display: block; vertical-align: middle; margin: 0 0 5px 0; padding: 0 6px 0 28px; width: 100%; } .daterangepicker .input-mini.active { border: 1px solid #08c; border-radius: 4px; } .daterangepicker .daterangepicker_input { position: relative; } .daterangepicker .daterangepicker_input i { position: absolute; left: 8px; top: 8px; } .daterangepicker.rtl .input-mini { padding-right: 28px; padding-left: 6px; } .daterangepicker.rtl .daterangepicker_input i { left: auto; right: 8px; } .daterangepicker .calendar-time { text-align: center; margin: 5px auto; line-height: 30px; position: relative; padding-left: 28px; } .daterangepicker .calendar-time select.disabled { color: #ccc; cursor: not-allowed; } .ranges { font-size: 11px; float: none; margin: 4px; text-align: left; } .ranges ul { list-style: none; margin: 0 auto; padding: 0; width: 100%; } .ranges li { font-size: 13px; background-color: #f5f5f5; border: 1px solid #f5f5f5; border-radius: 4px; color: #08c; padding: 3px 12px; margin-bottom: 8px; cursor: pointer; } .ranges li:hover { background-color: #08c; border: 1px solid #08c; color: #fff; } .ranges li.active { background-color: #08c; border: 1px solid #08c; color: #fff; } /* Larger Screen Styling */ @media (min-width: 564px) { .daterangepicker { width: auto; } .daterangepicker .ranges ul { width: 160px; } .daterangepicker.single .ranges ul { width: 100%; } .daterangepicker.single .calendar.left { clear: none; } .daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .calendar { float: left; } .daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .calendar { float: right; } .daterangepicker.ltr { direction: ltr; text-align: left; } .daterangepicker.ltr .calendar.left { clear: left; margin-right: 0; } .daterangepicker.ltr .calendar.left .calendar-table { border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; } .daterangepicker.ltr .calendar.right { margin-left: 0; } .daterangepicker.ltr .calendar.right .calendar-table { border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; } .daterangepicker.ltr .left .daterangepicker_input { padding-right: 12px; } .daterangepicker.ltr .calendar.left .calendar-table { padding-right: 12px; } .daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar { float: left; } .daterangepicker.rtl { direction: rtl; text-align: right; } .daterangepicker.rtl .calendar.left { clear: right; margin-left: 0; } .daterangepicker.rtl .calendar.left .calendar-table { border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; } .daterangepicker.rtl .calendar.right { margin-right: 0; } .daterangepicker.rtl .calendar.right .calendar-table { border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; } .daterangepicker.rtl .left .daterangepicker_input { padding-left: 12px; } .daterangepicker.rtl .calendar.left .calendar-table { padding-left: 12px; } .daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar { text-align: right; float: right; } } @media (min-width: 730px) { .daterangepicker .ranges { width: auto; } .daterangepicker.ltr .ranges { float: left; } .daterangepicker.rtl .ranges { float: right; } .daterangepicker .calendar.left { clear: none !important; } } ================================================ FILE: xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js ================================================ /** * @version: 2.1.27 * @author: Dan Grossman http://www.dangrossman.info/ * @copyright: Copyright (c) 2012-2017 Dan Grossman. All rights reserved. * @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php * @website: http://www.daterangepicker.com/ */ // Follow the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Make globaly available as well define(['moment', 'jquery'], function (moment, jquery) { if (!jquery.fn) jquery.fn = {}; // webpack server rendering return factory(moment, jquery); }); } else if (typeof module === 'object' && module.exports) { // Node / Browserify //isomorphic issue var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined; if (!jQuery) { jQuery = require('jquery'); if (!jQuery.fn) jQuery.fn = {}; } var moment = (typeof window != 'undefined' && typeof window.moment != 'undefined') ? window.moment : require('moment'); module.exports = factory(moment, jQuery); } else { // Browser globals root.daterangepicker = factory(root.moment, root.jQuery); } }(this, function(moment, $) { var DateRangePicker = function(element, options, cb) { //default settings for options this.parentEl = 'body'; this.element = $(element); this.startDate = moment().startOf('day'); this.endDate = moment().endOf('day'); this.minDate = false; this.maxDate = false; this.dateLimit = false; this.autoApply = false; this.singleDatePicker = false; this.showDropdowns = false; this.showWeekNumbers = false; this.showISOWeekNumbers = false; this.showCustomRangeLabel = true; this.timePicker = false; this.timePicker24Hour = false; this.timePickerIncrement = 1; this.timePickerSeconds = false; this.linkedCalendars = true; this.autoUpdateInput = true; this.alwaysShowCalendars = false; this.ranges = {}; this.opens = 'right'; if (this.element.hasClass('pull-right')) this.opens = 'left'; this.drops = 'down'; if (this.element.hasClass('dropup')) this.drops = 'up'; this.buttonClasses = 'btn btn-sm'; this.applyClass = 'btn-success'; this.cancelClass = 'btn-default'; this.locale = { direction: 'ltr', format: moment.localeData().longDateFormat('L'), separator: ' - ', applyLabel: 'Apply', cancelLabel: 'Cancel', weekLabel: 'W', customRangeLabel: 'Custom Range', daysOfWeek: moment.weekdaysMin(), monthNames: moment.monthsShort(), firstDay: moment.localeData().firstDayOfWeek() }; this.callback = function() { }; //some state information this.isShowing = false; this.leftCalendar = {}; this.rightCalendar = {}; //custom options from user if (typeof options !== 'object' || options === null) options = {}; //allow setting options with data attributes //data-api options will be overwritten with custom javascript options options = $.extend(this.element.data(), options); //html template for the picker UI if (typeof options.template !== 'string' && !(options.template instanceof $)) options.template = ''; this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); this.container = $(options.template).appendTo(this.parentEl); // // handle all the possible options overriding defaults // if (typeof options.locale === 'object') { if (typeof options.locale.direction === 'string') this.locale.direction = options.locale.direction; if (typeof options.locale.format === 'string') this.locale.format = options.locale.format; if (typeof options.locale.separator === 'string') this.locale.separator = options.locale.separator; if (typeof options.locale.daysOfWeek === 'object') this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); if (typeof options.locale.monthNames === 'object') this.locale.monthNames = options.locale.monthNames.slice(); if (typeof options.locale.firstDay === 'number') this.locale.firstDay = options.locale.firstDay; if (typeof options.locale.applyLabel === 'string') this.locale.applyLabel = options.locale.applyLabel; if (typeof options.locale.cancelLabel === 'string') this.locale.cancelLabel = options.locale.cancelLabel; if (typeof options.locale.weekLabel === 'string') this.locale.weekLabel = options.locale.weekLabel; if (typeof options.locale.customRangeLabel === 'string'){ //Support unicode chars in the custom range name. var elem = document.createElement('textarea'); elem.innerHTML = options.locale.customRangeLabel; var rangeHtml = elem.value; this.locale.customRangeLabel = rangeHtml; } } this.container.addClass(this.locale.direction); if (typeof options.startDate === 'string') this.startDate = moment(options.startDate, this.locale.format); if (typeof options.endDate === 'string') this.endDate = moment(options.endDate, this.locale.format); if (typeof options.minDate === 'string') this.minDate = moment(options.minDate, this.locale.format); if (typeof options.maxDate === 'string') this.maxDate = moment(options.maxDate, this.locale.format); if (typeof options.startDate === 'object') this.startDate = moment(options.startDate); if (typeof options.endDate === 'object') this.endDate = moment(options.endDate); if (typeof options.minDate === 'object') this.minDate = moment(options.minDate); if (typeof options.maxDate === 'object') this.maxDate = moment(options.maxDate); // sanity check for bad options if (this.minDate && this.startDate.isBefore(this.minDate)) this.startDate = this.minDate.clone(); // sanity check for bad options if (this.maxDate && this.endDate.isAfter(this.maxDate)) this.endDate = this.maxDate.clone(); if (typeof options.applyClass === 'string') this.applyClass = options.applyClass; if (typeof options.cancelClass === 'string') this.cancelClass = options.cancelClass; if (typeof options.dateLimit === 'object') this.dateLimit = options.dateLimit; if (typeof options.opens === 'string') this.opens = options.opens; if (typeof options.drops === 'string') this.drops = options.drops; if (typeof options.showWeekNumbers === 'boolean') this.showWeekNumbers = options.showWeekNumbers; if (typeof options.showISOWeekNumbers === 'boolean') this.showISOWeekNumbers = options.showISOWeekNumbers; if (typeof options.buttonClasses === 'string') this.buttonClasses = options.buttonClasses; if (typeof options.buttonClasses === 'object') this.buttonClasses = options.buttonClasses.join(' '); if (typeof options.showDropdowns === 'boolean') this.showDropdowns = options.showDropdowns; if (typeof options.showCustomRangeLabel === 'boolean') this.showCustomRangeLabel = options.showCustomRangeLabel; if (typeof options.singleDatePicker === 'boolean') { this.singleDatePicker = options.singleDatePicker; if (this.singleDatePicker) this.endDate = this.startDate.clone(); } if (typeof options.timePicker === 'boolean') this.timePicker = options.timePicker; if (typeof options.timePickerSeconds === 'boolean') this.timePickerSeconds = options.timePickerSeconds; if (typeof options.timePickerIncrement === 'number') this.timePickerIncrement = options.timePickerIncrement; if (typeof options.timePicker24Hour === 'boolean') this.timePicker24Hour = options.timePicker24Hour; if (typeof options.autoApply === 'boolean') this.autoApply = options.autoApply; if (typeof options.autoUpdateInput === 'boolean') this.autoUpdateInput = options.autoUpdateInput; if (typeof options.linkedCalendars === 'boolean') this.linkedCalendars = options.linkedCalendars; if (typeof options.isInvalidDate === 'function') this.isInvalidDate = options.isInvalidDate; if (typeof options.isCustomDate === 'function') this.isCustomDate = options.isCustomDate; if (typeof options.alwaysShowCalendars === 'boolean') this.alwaysShowCalendars = options.alwaysShowCalendars; // update day names order to firstDay if (this.locale.firstDay != 0) { var iterator = this.locale.firstDay; while (iterator > 0) { this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); iterator--; } } var start, end, range; //if no start/end dates set, check if an input element contains initial values if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { if ($(this.element).is('input[type=text]')) { var val = $(this.element).val(), split = val.split(this.locale.separator); start = end = null; if (split.length == 2) { start = moment(split[0], this.locale.format); end = moment(split[1], this.locale.format); } else if (this.singleDatePicker && val !== "") { start = moment(val, this.locale.format); end = moment(val, this.locale.format); } if (start !== null && end !== null) { this.setStartDate(start); this.setEndDate(end); } } } if (typeof options.ranges === 'object') { for (range in options.ranges) { if (typeof options.ranges[range][0] === 'string') start = moment(options.ranges[range][0], this.locale.format); else start = moment(options.ranges[range][0]); if (typeof options.ranges[range][1] === 'string') end = moment(options.ranges[range][1], this.locale.format); else end = moment(options.ranges[range][1]); // If the start or end date exceed those allowed by the minDate or dateLimit // options, shorten the range to the allowable period. if (this.minDate && start.isBefore(this.minDate)) start = this.minDate.clone(); var maxDate = this.maxDate; if (this.dateLimit && maxDate && start.clone().add(this.dateLimit).isAfter(maxDate)) maxDate = start.clone().add(this.dateLimit); if (maxDate && end.isAfter(maxDate)) end = maxDate.clone(); // If the end of the range is before the minimum or the start of the range is // after the maximum, don't display this range option at all. if ((this.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day')) || (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day'))) continue; //Support unicode chars in the range names. var elem = document.createElement('textarea'); elem.innerHTML = range; var rangeHtml = elem.value; this.ranges[rangeHtml] = [start, end]; } var list = '
    '; for (range in this.ranges) { list += '
  • ' + range + '
  • '; } if (this.showCustomRangeLabel) { list += '
  • ' + this.locale.customRangeLabel + '
  • '; } list += '
'; this.container.find('.ranges').prepend(list); } if (typeof cb === 'function') { this.callback = cb; } if (!this.timePicker) { this.startDate = this.startDate.startOf('day'); this.endDate = this.endDate.endOf('day'); this.container.find('.calendar-time').hide(); } //can't be used together for now if (this.timePicker && this.autoApply) this.autoApply = false; if (this.autoApply && typeof options.ranges !== 'object') { this.container.find('.ranges').hide(); } else if (this.autoApply) { this.container.find('.applyBtn, .cancelBtn').addClass('hide'); } if (this.singleDatePicker) { this.container.addClass('single'); this.container.find('.calendar.left').addClass('single'); this.container.find('.calendar.left').show(); this.container.find('.calendar.right').hide(); this.container.find('.daterangepicker_input input, .daterangepicker_input > i').hide(); if (this.timePicker) { this.container.find('.ranges ul').hide(); } else { this.container.find('.ranges').hide(); } } if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) { this.container.addClass('show-calendar'); } this.container.addClass('opens' + this.opens); //swap the position of the predefined ranges if opens right if (typeof options.ranges !== 'undefined' && this.opens == 'right') { this.container.find('.ranges').prependTo( this.container.find('.calendar.left').parent() ); } //apply CSS classes and labels to buttons this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses); if (this.applyClass.length) this.container.find('.applyBtn').addClass(this.applyClass); if (this.cancelClass.length) this.container.find('.cancelBtn').addClass(this.cancelClass); this.container.find('.applyBtn').html(this.locale.applyLabel); this.container.find('.cancelBtn').html(this.locale.cancelLabel); // // event listeners // this.container.find('.calendar') .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) .on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) .on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this)) .on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this)) .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this)) .on('click.daterangepicker', '.daterangepicker_input input', $.proxy(this.showCalendars, this)) .on('focus.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsFocused, this)) .on('blur.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsBlurred, this)) .on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this)) .on('keydown.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsKeydown, this)); this.container.find('.ranges') .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) .on('mouseenter.daterangepicker', 'li', $.proxy(this.hoverRange, this)) .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); if (this.element.is('input') || this.element.is('button')) { this.element.on({ 'click.daterangepicker': $.proxy(this.show, this), 'focus.daterangepicker': $.proxy(this.show, this), 'keyup.daterangepicker': $.proxy(this.elementChanged, this), 'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility }); } else { this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this)); } // // if attached to a text input, set the initial value // if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); this.element.trigger('change'); } else if (this.element.is('input') && this.autoUpdateInput) { this.element.val(this.startDate.format(this.locale.format)); this.element.trigger('change'); } }; DateRangePicker.prototype = { constructor: DateRangePicker, setStartDate: function(startDate) { if (typeof startDate === 'string') this.startDate = moment(startDate, this.locale.format); if (typeof startDate === 'object') this.startDate = moment(startDate); if (!this.timePicker) this.startDate = this.startDate.startOf('day'); if (this.timePicker && this.timePickerIncrement) this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); if (this.minDate && this.startDate.isBefore(this.minDate)) { this.startDate = this.minDate.clone(); if (this.timePicker && this.timePickerIncrement) this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); } if (this.maxDate && this.startDate.isAfter(this.maxDate)) { this.startDate = this.maxDate.clone(); if (this.timePicker && this.timePickerIncrement) this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); } if (!this.isShowing) this.updateElement(); this.updateMonthsInView(); }, setEndDate: function(endDate) { if (typeof endDate === 'string') this.endDate = moment(endDate, this.locale.format); if (typeof endDate === 'object') this.endDate = moment(endDate); if (!this.timePicker) this.endDate = this.endDate.add(1,'d').startOf('day').subtract(1,'second'); if (this.timePicker && this.timePickerIncrement) this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); if (this.endDate.isBefore(this.startDate)) this.endDate = this.startDate.clone(); if (this.maxDate && this.endDate.isAfter(this.maxDate)) this.endDate = this.maxDate.clone(); if (this.dateLimit && this.startDate.clone().add(this.dateLimit).isBefore(this.endDate)) this.endDate = this.startDate.clone().add(this.dateLimit); this.previousRightTime = this.endDate.clone(); if (!this.isShowing) this.updateElement(); this.updateMonthsInView(); }, isInvalidDate: function() { return false; }, isCustomDate: function() { return false; }, updateView: function() { if (this.timePicker) { this.renderTimePicker('left'); this.renderTimePicker('right'); if (!this.endDate) { this.container.find('.right .calendar-time select').attr('disabled', 'disabled').addClass('disabled'); } else { this.container.find('.right .calendar-time select').removeAttr('disabled').removeClass('disabled'); } } if (this.endDate) { this.container.find('input[name="daterangepicker_end"]').removeClass('active'); this.container.find('input[name="daterangepicker_start"]').addClass('active'); } else { this.container.find('input[name="daterangepicker_end"]').addClass('active'); this.container.find('input[name="daterangepicker_start"]').removeClass('active'); } this.updateMonthsInView(); this.updateCalendars(); this.updateFormInputs(); }, updateMonthsInView: function() { if (this.endDate) { //if both dates are visible already, do nothing if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) && (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) ) { return; } this.leftCalendar.month = this.startDate.clone().date(2); if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) { this.rightCalendar.month = this.endDate.clone().date(2); } else { this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); } } else { if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) { this.leftCalendar.month = this.startDate.clone().date(2); this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); } } if (this.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) { this.rightCalendar.month = this.maxDate.clone().date(2); this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month'); } }, updateCalendars: function() { if (this.timePicker) { var hour, minute, second; if (this.endDate) { hour = parseInt(this.container.find('.left .hourselect').val(), 10); minute = parseInt(this.container.find('.left .minuteselect').val(), 10); second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; if (!this.timePicker24Hour) { var ampm = this.container.find('.left .ampmselect').val(); if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; } } else { hour = parseInt(this.container.find('.right .hourselect').val(), 10); minute = parseInt(this.container.find('.right .minuteselect').val(), 10); second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; if (!this.timePicker24Hour) { var ampm = this.container.find('.right .ampmselect').val(); if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; } } this.leftCalendar.month.hour(hour).minute(minute).second(second); this.rightCalendar.month.hour(hour).minute(minute).second(second); } this.renderCalendar('left'); this.renderCalendar('right'); //highlight any predefined range matching the current start and end dates this.container.find('.ranges li').removeClass('active'); if (this.endDate == null) return; this.calculateChosenLabel(); }, renderCalendar: function(side) { // // Build the matrix of dates that will populate the calendar // var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar; var month = calendar.month.month(); var year = calendar.month.year(); var hour = calendar.month.hour(); var minute = calendar.month.minute(); var second = calendar.month.second(); var daysInMonth = moment([year, month]).daysInMonth(); var firstDay = moment([year, month, 1]); var lastDay = moment([year, month, daysInMonth]); var lastMonth = moment(firstDay).subtract(1, 'month').month(); var lastYear = moment(firstDay).subtract(1, 'month').year(); var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); var dayOfWeek = firstDay.day(); //initialize a 6 rows x 7 columns array for the calendar var calendar = []; calendar.firstDay = firstDay; calendar.lastDay = lastDay; for (var i = 0; i < 6; i++) { calendar[i] = []; } //populate the calendar with date objects var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; if (startDay > daysInLastMonth) startDay -= 7; if (dayOfWeek == this.locale.firstDay) startDay = daysInLastMonth - 6; var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]); var col, row; for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { if (i > 0 && col % 7 === 0) { col = 0; row++; } calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); curDate.hour(12); if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { calendar[row][col] = this.minDate.clone(); } if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { calendar[row][col] = this.maxDate.clone(); } } //make the calendar object available to hoverDate/clickDate if (side == 'left') { this.leftCalendar.calendar = calendar; } else { this.rightCalendar.calendar = calendar; } // // Display the calendar // var minDate = side == 'left' ? this.minDate : this.startDate; var maxDate = this.maxDate; var selected = side == 'left' ? this.startDate : this.endDate; var arrow = this.locale.direction == 'ltr' ? {left: 'chevron-left', right: 'chevron-right'} : {left: 'chevron-right', right: 'chevron-left'}; var html = ''; html += ''; html += ''; // add empty cell for week number if (this.showWeekNumbers || this.showISOWeekNumbers) html += ''; if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) { html += ''; } else { html += ''; } var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); if (this.showDropdowns) { var currentMonth = calendar[1][1].month(); var currentYear = calendar[1][1].year(); var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); var minYear = (minDate && minDate.year()) || (currentYear - 50); var inMinYear = currentYear == minYear; var inMaxYear = currentYear == maxYear; var monthHtml = '"; var yearHtml = ''; dateHtml = monthHtml + yearHtml; } html += ''; if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) { html += ''; } else { html += ''; } html += ''; html += ''; // add week number label if (this.showWeekNumbers || this.showISOWeekNumbers) html += ''; $.each(this.locale.daysOfWeek, function(index, dayOfWeek) { html += ''; }); html += ''; html += ''; html += ''; //adjust maxDate to reflect the dateLimit setting in order to //grey out end dates beyond the dateLimit if (this.endDate == null && this.dateLimit) { var maxLimit = this.startDate.clone().add(this.dateLimit).endOf('day'); if (!maxDate || maxLimit.isBefore(maxDate)) { maxDate = maxLimit; } } for (var row = 0; row < 6; row++) { html += ''; // add week number if (this.showWeekNumbers) html += ''; else if (this.showISOWeekNumbers) html += ''; for (var col = 0; col < 7; col++) { var classes = []; //highlight today's date if (calendar[row][col].isSame(new Date(), "day")) classes.push('today'); //highlight weekends if (calendar[row][col].isoWeekday() > 5) classes.push('weekend'); //grey out the dates in other months displayed at beginning and end of this calendar if (calendar[row][col].month() != calendar[1][1].month()) classes.push('off'); //don't allow selection of dates before the minimum date if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day')) classes.push('off', 'disabled'); //don't allow selection of dates after the maximum date if (maxDate && calendar[row][col].isAfter(maxDate, 'day')) classes.push('off', 'disabled'); //don't allow selection of date if a custom function decides it's invalid if (this.isInvalidDate(calendar[row][col])) classes.push('off', 'disabled'); //highlight the currently selected start date if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) classes.push('active', 'start-date'); //highlight the currently selected end date if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) classes.push('active', 'end-date'); //highlight dates in-between the selected dates if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate) classes.push('in-range'); //apply custom classes for this date var isCustom = this.isCustomDate(calendar[row][col]); if (isCustom !== false) { if (typeof isCustom === 'string') classes.push(isCustom); else Array.prototype.push.apply(classes, isCustom); } var cname = '', disabled = false; for (var i = 0; i < classes.length; i++) { cname += classes[i] + ' '; if (classes[i] == 'disabled') disabled = true; } if (!disabled) cname += 'available'; html += ''; } html += ''; } html += ''; html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][0].isoWeek() + '' + calendar[row][col].date() + '
'; this.container.find('.calendar.' + side + ' .calendar-table').html(html); }, renderTimePicker: function(side) { // Don't bother updating the time picker if it's currently disabled // because an end date hasn't been clicked yet if (side == 'right' && !this.endDate) return; var html, selected, minDate, maxDate = this.maxDate; if (this.dateLimit && (!this.maxDate || this.startDate.clone().add(this.dateLimit).isAfter(this.maxDate))) maxDate = this.startDate.clone().add(this.dateLimit); if (side == 'left') { selected = this.startDate.clone(); minDate = this.minDate; } else if (side == 'right') { selected = this.endDate.clone(); minDate = this.startDate; //Preserve the time already selected var timeSelector = this.container.find('.calendar.right .calendar-time div'); if (timeSelector.html() != '') { selected.hour(timeSelector.find('.hourselect option:selected').val() || selected.hour()); selected.minute(timeSelector.find('.minuteselect option:selected').val() || selected.minute()); selected.second(timeSelector.find('.secondselect option:selected').val() || selected.second()); if (!this.timePicker24Hour) { var ampm = timeSelector.find('.ampmselect option:selected').val(); if (ampm === 'PM' && selected.hour() < 12) selected.hour(selected.hour() + 12); if (ampm === 'AM' && selected.hour() === 12) selected.hour(0); } } if (selected.isBefore(this.startDate)) selected = this.startDate.clone(); if (maxDate && selected.isAfter(maxDate)) selected = maxDate.clone(); } // // hours // html = ' '; // // minutes // html += ': '; // // seconds // if (this.timePickerSeconds) { html += ': '; } // // AM/PM // if (!this.timePicker24Hour) { html += ''; } this.container.find('.calendar.' + side + ' .calendar-time div').html(html); }, updateFormInputs: function() { //ignore mouse movements while an above-calendar text input has focus if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) return; this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.locale.format)); if (this.endDate) this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.locale.format)); if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) { this.container.find('button.applyBtn').removeAttr('disabled'); } else { this.container.find('button.applyBtn').attr('disabled', 'disabled'); } }, move: function() { var parentOffset = { top: 0, left: 0 }, containerTop; var parentRightEdge = $(window).width(); if (!this.parentEl.is('body')) { parentOffset = { top: this.parentEl.offset().top - this.parentEl.scrollTop(), left: this.parentEl.offset().left - this.parentEl.scrollLeft() }; parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; } if (this.drops == 'up') containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; else containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('dropup'); if (this.opens == 'left') { this.container.css({ top: containerTop, right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), left: 'auto' }); if (this.container.offset().left < 0) { this.container.css({ right: 'auto', left: 9 }); } } else if (this.opens == 'center') { this.container.css({ top: containerTop, left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 - this.container.outerWidth() / 2, right: 'auto' }); if (this.container.offset().left < 0) { this.container.css({ right: 'auto', left: 9 }); } } else { this.container.css({ top: containerTop, left: this.element.offset().left - parentOffset.left, right: 'auto' }); if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { this.container.css({ left: 'auto', right: 0 }); } } }, show: function(e) { if (this.isShowing) return; // Create a click proxy that is private to this instance of datepicker, for unbinding this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this); // Bind global datepicker mousedown for hiding and $(document) .on('mousedown.daterangepicker', this._outsideClickProxy) // also support mobile devices .on('touchend.daterangepicker', this._outsideClickProxy) // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) // and also close when focus changes to outside the picker (eg. tabbing between controls) .on('focusin.daterangepicker', this._outsideClickProxy); // Reposition the picker if the window is resized while it's open $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this)); this.oldStartDate = this.startDate.clone(); this.oldEndDate = this.endDate.clone(); this.previousRightTime = this.endDate.clone(); this.updateView(); this.container.show(); this.move(); this.element.trigger('show.daterangepicker', this); this.isShowing = true; }, hide: function(e) { if (!this.isShowing) return; //incomplete date selection, revert to last values if (!this.endDate) { this.startDate = this.oldStartDate.clone(); this.endDate = this.oldEndDate.clone(); } //if a new date range was selected, invoke the user callback function if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) this.callback(this.startDate, this.endDate, this.chosenLabel); //if picker is attached to a text input, update it this.updateElement(); $(document).off('.daterangepicker'); $(window).off('.daterangepicker'); this.container.hide(); this.element.trigger('hide.daterangepicker', this); this.isShowing = false; }, toggle: function(e) { if (this.isShowing) { this.hide(); } else { this.show(); } }, outsideClick: function(e) { var target = $(e.target); // if the page is clicked anywhere except within the daterangerpicker/button // itself then call this.hide() if ( // ie modal dialog fix e.type == "focusin" || target.closest(this.element).length || target.closest(this.container).length || target.closest('.calendar-table').length ) return; this.hide(); this.element.trigger('outsideClick.daterangepicker', this); }, showCalendars: function() { this.container.addClass('show-calendar'); this.move(); this.element.trigger('showCalendar.daterangepicker', this); }, hideCalendars: function() { this.container.removeClass('show-calendar'); this.element.trigger('hideCalendar.daterangepicker', this); }, hoverRange: function(e) { //ignore mouse movements while an above-calendar text input has focus if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) return; var label = e.target.getAttribute('data-range-key'); if (label == this.locale.customRangeLabel) { this.updateView(); } else { var dates = this.ranges[label]; this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.locale.format)); this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.locale.format)); } }, clickRange: function(e) { var label = e.target.getAttribute('data-range-key'); this.chosenLabel = label; if (label == this.locale.customRangeLabel) { this.showCalendars(); } else { var dates = this.ranges[label]; this.startDate = dates[0]; this.endDate = dates[1]; if (!this.timePicker) { this.startDate.startOf('day'); this.endDate.endOf('day'); } if (!this.alwaysShowCalendars) this.hideCalendars(); this.clickApply(); } }, clickPrev: function(e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { this.leftCalendar.month.subtract(1, 'month'); if (this.linkedCalendars) this.rightCalendar.month.subtract(1, 'month'); } else { this.rightCalendar.month.subtract(1, 'month'); } this.updateCalendars(); }, clickNext: function(e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { this.leftCalendar.month.add(1, 'month'); } else { this.rightCalendar.month.add(1, 'month'); if (this.linkedCalendars) this.leftCalendar.month.add(1, 'month'); } this.updateCalendars(); }, hoverDate: function(e) { //ignore mouse movements while an above-calendar text input has focus //if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) // return; //ignore dates that can't be selected if (!$(e.target).hasClass('available')) return; //have the text inputs above calendars reflect the date being hovered over var title = $(e.target).attr('data-title'); var row = title.substr(1, 1); var col = title.substr(3, 1); var cal = $(e.target).parents('.calendar'); var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; if (this.endDate && !this.container.find('input[name=daterangepicker_start]').is(":focus")) { this.container.find('input[name=daterangepicker_start]').val(date.format(this.locale.format)); } else if (!this.endDate && !this.container.find('input[name=daterangepicker_end]').is(":focus")) { this.container.find('input[name=daterangepicker_end]').val(date.format(this.locale.format)); } //highlight the dates between the start date and the date being hovered as a potential end date var leftCalendar = this.leftCalendar; var rightCalendar = this.rightCalendar; var startDate = this.startDate; if (!this.endDate) { this.container.find('.calendar tbody td').each(function(index, el) { //skip week numbers, only look at dates if ($(el).hasClass('week')) return; var title = $(el).attr('data-title'); var row = title.substr(1, 1); var col = title.substr(3, 1); var cal = $(el).parents('.calendar'); var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col]; if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) { $(el).addClass('in-range'); } else { $(el).removeClass('in-range'); } }); } }, clickDate: function(e) { if (!$(e.target).hasClass('available')) return; var title = $(e.target).attr('data-title'); var row = title.substr(1, 1); var col = title.substr(3, 1); var cal = $(e.target).parents('.calendar'); var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; // // this function needs to do a few things: // * alternate between selecting a start and end date for the range, // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date // * if autoapply is enabled, and an end date was chosen, apply the selection // * if single date picker mode, and time picker isn't enabled, apply the selection immediately // * if one of the inputs above the calendars was focused, cancel that manual input // if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start if (this.timePicker) { var hour = parseInt(this.container.find('.left .hourselect').val(), 10); if (!this.timePicker24Hour) { var ampm = this.container.find('.left .ampmselect').val(); if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; } var minute = parseInt(this.container.find('.left .minuteselect').val(), 10); var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; date = date.clone().hour(hour).minute(minute).second(second); } this.endDate = null; this.setStartDate(date.clone()); } else if (!this.endDate && date.isBefore(this.startDate)) { //special case: clicking the same date for start/end, //but the time of the end date is before the start date this.setEndDate(this.startDate.clone()); } else { // picking end if (this.timePicker) { var hour = parseInt(this.container.find('.right .hourselect').val(), 10); if (!this.timePicker24Hour) { var ampm = this.container.find('.right .ampmselect').val(); if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; } var minute = parseInt(this.container.find('.right .minuteselect').val(), 10); var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; date = date.clone().hour(hour).minute(minute).second(second); } this.setEndDate(date.clone()); if (this.autoApply) { this.calculateChosenLabel(); this.clickApply(); } } if (this.singleDatePicker) { this.setEndDate(this.startDate); if (!this.timePicker) this.clickApply(); } this.updateView(); //This is to cancel the blur event handler if the mouse was in one of the inputs e.stopPropagation(); }, calculateChosenLabel: function () { var customRange = true; var i = 0; for (var range in this.ranges) { if (this.timePicker) { var format = this.timePickerSeconds ? "YYYY-MM-DD hh:mm:ss" : "YYYY-MM-DD hh:mm"; //ignore times when comparing dates if time picker seconds is not enabled if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) { customRange = false; this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); break; } } else { //ignore times when comparing dates if time picker is not enabled if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { customRange = false; this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); break; } } i++; } if (customRange) { if (this.showCustomRangeLabel) { this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); } else { this.chosenLabel = null; } this.showCalendars(); } }, clickApply: function(e) { this.hide(); this.element.trigger('apply.daterangepicker', this); }, clickCancel: function(e) { this.startDate = this.oldStartDate; this.endDate = this.oldEndDate; this.hide(); this.element.trigger('cancel.daterangepicker', this); }, monthOrYearChanged: function(e) { var isLeft = $(e.target).closest('.calendar').hasClass('left'), leftOrRight = isLeft ? 'left' : 'right', cal = this.container.find('.calendar.'+leftOrRight); // Month must be Number for new moment versions var month = parseInt(cal.find('.monthselect').val(), 10); var year = cal.find('.yearselect').val(); if (!isLeft) { if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { month = this.startDate.month(); year = this.startDate.year(); } } if (this.minDate) { if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { month = this.minDate.month(); year = this.minDate.year(); } } if (this.maxDate) { if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { month = this.maxDate.month(); year = this.maxDate.year(); } } if (isLeft) { this.leftCalendar.month.month(month).year(year); if (this.linkedCalendars) this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); } else { this.rightCalendar.month.month(month).year(year); if (this.linkedCalendars) this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); } this.updateCalendars(); }, timeChanged: function(e) { var cal = $(e.target).closest('.calendar'), isLeft = cal.hasClass('left'); var hour = parseInt(cal.find('.hourselect').val(), 10); var minute = parseInt(cal.find('.minuteselect').val(), 10); var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0; if (!this.timePicker24Hour) { var ampm = cal.find('.ampmselect').val(); if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; } if (isLeft) { var start = this.startDate.clone(); start.hour(hour); start.minute(minute); start.second(second); this.setStartDate(start); if (this.singleDatePicker) { this.endDate = this.startDate.clone(); } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { this.setEndDate(start.clone()); } } else if (this.endDate) { var end = this.endDate.clone(); end.hour(hour); end.minute(minute); end.second(second); this.setEndDate(end); } //update the calendars so all clickable dates reflect the new time component this.updateCalendars(); //update the form inputs above the calendars with the new time this.updateFormInputs(); //re-render the time pickers because changing one selection can affect what's enabled in another this.renderTimePicker('left'); this.renderTimePicker('right'); }, formInputsChanged: function(e) { var isRight = $(e.target).closest('.calendar').hasClass('right'); var start = moment(this.container.find('input[name="daterangepicker_start"]').val(), this.locale.format); var end = moment(this.container.find('input[name="daterangepicker_end"]').val(), this.locale.format); if (start.isValid() && end.isValid()) { if (isRight && end.isBefore(start)) start = end.clone(); this.setStartDate(start); this.setEndDate(end); if (isRight) { this.container.find('input[name="daterangepicker_start"]').val(this.startDate.format(this.locale.format)); } else { this.container.find('input[name="daterangepicker_end"]').val(this.endDate.format(this.locale.format)); } } this.updateView(); }, formInputsFocused: function(e) { // Highlight the focused input this.container.find('input[name="daterangepicker_start"], input[name="daterangepicker_end"]').removeClass('active'); $(e.target).addClass('active'); // Set the state such that if the user goes back to using a mouse, // the calendars are aware we're selecting the end of the range, not // the start. This allows someone to edit the end of a date range without // re-selecting the beginning, by clicking on the end date input then // using the calendar. var isRight = $(e.target).closest('.calendar').hasClass('right'); if (isRight) { this.endDate = null; this.setStartDate(this.startDate.clone()); this.updateView(); } }, formInputsBlurred: function(e) { // this function has one purpose right now: if you tab from the first // text input to the second in the UI, the endDate is nulled so that // you can click another, but if you tab out without clicking anything // or changing the input value, the old endDate should be retained if (!this.endDate) { var val = this.container.find('input[name="daterangepicker_end"]').val(); var end = moment(val, this.locale.format); if (end.isValid()) { this.setEndDate(end); this.updateView(); } } }, formInputsKeydown: function(e) { // This function ensures that if the 'enter' key was pressed in the input, then the calendars // are updated with the startDate and endDate. // This behaviour is automatic in Chrome/Firefox/Edge but not in IE 11 hence why this exists. // Other browsers and versions of IE are untested and the behaviour is unknown. if (e.keyCode === 13) { // Prevent the calendar from being updated twice on Chrome/Firefox/Edge e.preventDefault(); this.formInputsChanged(e); } }, elementChanged: function() { if (!this.element.is('input')) return; if (!this.element.val().length) return; var dateString = this.element.val().split(this.locale.separator), start = null, end = null; if (dateString.length === 2) { start = moment(dateString[0], this.locale.format); end = moment(dateString[1], this.locale.format); } if (this.singleDatePicker || start === null || end === null) { start = moment(this.element.val(), this.locale.format); end = start; } if (!start.isValid() || !end.isValid()) return; this.setStartDate(start); this.setEndDate(end); this.updateView(); }, keydown: function(e) { //hide on tab or enter if ((e.keyCode === 9) || (e.keyCode === 13)) { this.hide(); } //hide on esc and prevent propagation if (e.keyCode === 27) { e.preventDefault(); e.stopPropagation(); this.hide(); } }, updateElement: function() { if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); this.element.trigger('change'); } else if (this.element.is('input') && this.autoUpdateInput) { this.element.val(this.startDate.format(this.locale.format)); this.element.trigger('change'); } }, remove: function() { this.container.remove(); this.element.off('.daterangepicker'); this.element.removeData(); } }; $.fn.daterangepicker = function(options, callback) { var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options); this.each(function() { var el = $(this); if (el.data('daterangepicker')) el.data('daterangepicker').remove(); el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback)); }); return this; }; return DateRangePicker; })); ================================================ FILE: xxl-job-admin/src/main/resources/static/adminlte/bower_components/ckeditor/ckeditor.js ================================================ /* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ (function(){window.CKEDITOR&&window.CKEDITOR.dom||(window.CKEDITOR||(window.CKEDITOR=function(){var a=/(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i,f={timestamp:"J5S9",version:"4.12.1 (Standard)",revision:"64749bb245",rnd:Math.floor(900*Math.random())+100,_:{pending:[],basePathSrcPattern:a},status:"unloaded",basePath:function(){var b=window.CKEDITOR_BASEPATH||"";if(!b)for(var c=document.getElementsByTagName("script"),f=0;fe.getListenerIndex(d)){e=e.listeners;f||(f=this);isNaN(g)&&(g=10);var n=this;h.fn=d;h.priority=g;for(var q=e.length-1;0<=q;q--)if(e[q].priority<=g)return e.splice(q+1,0,h),{removeListener:m};e.unshift(h)}return{removeListener:m}},once:function(){var a=Array.prototype.slice.call(arguments),b=a[1];a[1]=function(a){a.removeListener();return b.apply(this, arguments)};return this.on.apply(this,a)},capture:function(){CKEDITOR.event.useCapture=1;var a=this.on.apply(this,arguments);CKEDITOR.event.useCapture=0;return a},fire:function(){var a=0,b=function(){a=1},l=0,k=function(){l=1};return function(g,h,m){var e=f(this)[g];g=a;var n=l;a=l=0;if(e){var q=e.listeners;if(q.length)for(var q=q.slice(0),y,u=0;udocument.documentMode),mobile:-1c||b.quirks);b.gecko&&(f=a.match(/rv:([\d\.]+)/))&&(f=f[1].split("."),c=1E4*f[0]+100*(f[1]||0)+1*(f[2]||0));b.air&&(c=parseFloat(a.match(/ adobeair\/(\d+)/)[1]));b.webkit&&(c=parseFloat(a.match(/ applewebkit\/(\d+)/)[1]));b.version=c;b.isCompatible=!(b.ie&&7>c)&&!(b.gecko&&4E4>c)&&!(b.webkit&& 534>c);b.hidpi=2<=window.devicePixelRatio;b.needsBrFiller=b.gecko||b.webkit||b.ie&&10c;b.cssClass="cke_browser_"+(b.ie?"ie":b.gecko?"gecko":b.webkit?"webkit":"unknown");b.quirks&&(b.cssClass+=" cke_browser_quirks");b.ie&&(b.cssClass+=" cke_browser_ie"+(b.quirks?"6 cke_browser_iequirks":b.version));b.air&&(b.cssClass+=" cke_browser_air");b.iOS&&(b.cssClass+=" cke_browser_ios");b.hidpi&&(b.cssClass+=" cke_hidpi");return b}()),"unloaded"==CKEDITOR.status&&function(){CKEDITOR.event.implementOn(CKEDITOR); CKEDITOR.loadFullCore=function(){if("basic_ready"!=CKEDITOR.status)CKEDITOR.loadFullCore._load=1;else{delete CKEDITOR.loadFullCore;var a=document.createElement("script");a.type="text/javascript";a.src=CKEDITOR.basePath+"ckeditor.js";document.getElementsByTagName("head")[0].appendChild(a)}};CKEDITOR.loadFullCoreTimeout=0;CKEDITOR.add=function(a){(this._.pending||(this._.pending=[])).push(a)};(function(){CKEDITOR.domReady(function(){var a=CKEDITOR.loadFullCore,f=CKEDITOR.loadFullCoreTimeout;a&&(CKEDITOR.status= "basic_ready",a&&a._load?a():f&&setTimeout(function(){CKEDITOR.loadFullCore&&CKEDITOR.loadFullCore()},1E3*f))})})();CKEDITOR.status="basic_loaded"}(),"use strict",CKEDITOR.VERBOSITY_WARN=1,CKEDITOR.VERBOSITY_ERROR=2,CKEDITOR.verbosity=CKEDITOR.VERBOSITY_WARN|CKEDITOR.VERBOSITY_ERROR,CKEDITOR.warn=function(a,f){CKEDITOR.verbosity&CKEDITOR.VERBOSITY_WARN&&CKEDITOR.fire("log",{type:"warn",errorCode:a,additionalData:f})},CKEDITOR.error=function(a,f){CKEDITOR.verbosity&CKEDITOR.VERBOSITY_ERROR&&CKEDITOR.fire("log", {type:"error",errorCode:a,additionalData:f})},CKEDITOR.on("log",function(a){if(window.console&&window.console.log){var f=console[a.data.type]?a.data.type:"log",b=a.data.errorCode;if(a=a.data.additionalData)console[f]("[CKEDITOR] Error code: "+b+".",a);else console[f]("[CKEDITOR] Error code: "+b+".");console[f]("[CKEDITOR] For more information about this error go to https://ckeditor.com/docs/ckeditor4/latest/guide/dev_errors.html#"+b)}},null,null,999),CKEDITOR.dom={},function(){function a(a,e,b){this._minInterval= a;this._context=b;this._lastOutput=this._scheduledTimer=0;this._output=CKEDITOR.tools.bind(e,b||{});var g=this;this.input=function(){function a(){g._lastOutput=(new Date).getTime();g._scheduledTimer=0;g._call()}if(!g._scheduledTimer||!1!==g._reschedule()){var e=(new Date).getTime()-g._lastOutput;e/g,k=/|\s) /g,function(a,e){return e+"\x26nbsp;"}).replace(/ (?=<)/g,"\x26nbsp;")},getNextNumber:function(){var a=0;return function(){return++a}}(),getNextId:function(){return"cke_"+this.getNextNumber()},getUniqueId:function(){for(var a="e",e=0;8>e;e++)a+=Math.floor(65536*(1+Math.random())).toString(16).substring(1);return a},override:function(a,e){var g=e(a);g.prototype=a.prototype;return g},setTimeout:function(a,e,g,b,h){h||(h=window);g||(g= h);return h.setTimeout(function(){b?a.apply(g,[].concat(b)):a.apply(g)},e||0)},throttle:function(a,e,g){return new this.buffers.throttle(a,e,g)},trim:function(){var a=/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g;return function(e){return e.replace(a,"")}}(),ltrim:function(){var a=/^[ \t\n\r]+/g;return function(e){return e.replace(a,"")}}(),rtrim:function(){var a=/[ \t\n\r]+$/g;return function(e){return e.replace(a,"")}}(),indexOf:function(a,e){if("function"==typeof e)for(var g=0,b=a.length;gparseFloat(e);g&&(e=e.replace("-",""));a.setStyle("width",e);e=a.$.clientWidth;return g?-e:e}return e}}(),repeat:function(a, e){return Array(e+1).join(a)},tryThese:function(){for(var a,e=0,g=arguments.length;ee;e++)a[e]=("0"+parseInt(a[e],10).toString(16)).slice(-2);return"#"+a.join("")})},normalizeHex:function(a){return a.replace(/#(([0-9a-f]{3}){1,2})($|;|\s+)/gi,function(a,e,g,b){a=e.toLowerCase();3==a.length&&(a=a.split(""),a=[a[0],a[0],a[1],a[1],a[2],a[2]].join(""));return"#"+a+b})},parseCssText:function(a,e,g){var b={};g&&(a=(new CKEDITOR.dom.element("span")).setAttribute("style",a).getAttribute("style")||"");a&&(a=CKEDITOR.tools.normalizeHex(CKEDITOR.tools.convertRgbToHex(a))); if(!a||";"==a)return b;a.replace(/"/g,'"').replace(/\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g,function(a,g,h){e&&(g=g.toLowerCase(),"font-family"==g&&(h=h.replace(/\s*,\s*/g,",")),h=CKEDITOR.tools.trim(h));b[g]=h});return b},writeCssText:function(a,e){var g,b=[];for(g in a)b.push(g+":"+a[g]);e&&b.sort();return b.join("; ")},objectCompare:function(a,e,g){var b;if(!a&&!e)return!0;if(!a||!e)return!1;for(b in a)if(a[b]!=e[b])return!1;if(!g)for(b in e)if(a[b]!=e[b])return!1;return!0},objectKeys:function(a){return CKEDITOR.tools.object.keys(a)}, convertArrayToObject:function(a,e){var g={};1==arguments.length&&(e=!0);for(var b=0,h=a.length;bg;g++)a.push(Math.floor(256*Math.random())); for(g=0;gCKEDITOR.env.version||CKEDITOR.env.ie6Compat)?4===a.button?CKEDITOR.MOUSE_BUTTON_MIDDLE:1===a.button? CKEDITOR.MOUSE_BUTTON_LEFT:CKEDITOR.MOUSE_BUTTON_RIGHT:a.button:!1},convertHexStringToBytes:function(a){var e=[],g=a.length/2,b;for(b=0;bc)for(m=c;3>m;m++)h[m]=0;d[0]=(h[0]&252)>>2;d[1]=(h[0]&3)<<4|h[1]>>4;d[2]=(h[1]&15)<<2|(h[2]&192)>>6;d[3]=h[2]&63;for(m=0;4>m;m++)e=m<=c?e+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(d[m]): e+"\x3d"}return e},style:{parse:{_colors:{aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aqua:"#00FFFF",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blue:"#0000FF",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9", darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",fuchsia:"#FF00FF", gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",gray:"#808080",green:"#008000",greenyellow:"#ADFF2F",grey:"#808080",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgray:"#D3D3D3",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1", lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",lime:"#00FF00",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",maroon:"#800000",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA", mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",navy:"#000080",oldlace:"#FDF5E6",olive:"#808000",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",purple:"#800080",rebeccapurple:"#663399",red:"#FF0000",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072", sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",silver:"#C0C0C0",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",teal:"#008080",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",windowtext:"windowtext",wheat:"#F5DEB3",white:"#FFFFFF",whitesmoke:"#F5F5F5",yellow:"#FFFF00",yellowgreen:"#9ACD32"},_borderStyle:"none hidden dotted dashed solid double groove ridge inset outset".split(" "), _widthRegExp:/^(thin|medium|thick|[\+-]?\d+(\.\d+)?[a-z%]+|[\+-]?0+(\.0+)?|\.\d+[a-z%]+)$/,_rgbaRegExp:/rgba?\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*(?:,\s*[0-9.]+\s*)?\)/gi,_hslaRegExp:/hsla?\(\s*[0-9.]+\s*,\s*\d+%\s*,\s*\d+%\s*(?:,\s*[0-9.]+\s*)?\)/gi,background:function(a){var e={},g=this._findColor(a);g.length&&(e.color=g[0],CKEDITOR.tools.array.forEach(g,function(e){a=a.replace(e,"")}));if(a=CKEDITOR.tools.trim(a))e.unprocessed=a;return e},margin:function(a){return CKEDITOR.tools.style.parse.sideShorthand(a, function(a){return a.match(/(?:\-?[\.\d]+(?:%|\w*)|auto|inherit|initial|unset|revert)/g)||["0px"]})},sideShorthand:function(a,e){function g(a){b.top=h[a[0]];b.right=h[a[1]];b.bottom=h[a[2]];b.left=h[a[3]]}var b={},h=e?e(a):a.split(/\s+/);switch(h.length){case 1:g([0,0,0,0]);break;case 2:g([0,1,0,1]);break;case 3:g([0,1,2,1]);break;case 4:g([0,1,2,3])}return b},border:function(a){return CKEDITOR.tools.style.border.fromCssRule(a)},_findColor:function(a){var e=[],g=CKEDITOR.tools.array,e=e.concat(a.match(this._rgbaRegExp)|| []),e=e.concat(a.match(this._hslaRegExp)||[]);return e=e.concat(g.filter(a.split(/\s+/),function(a){return a.match(/^\#[a-f0-9]{3}(?:[a-f0-9]{3})?$/gi)?!0:a.toLowerCase()in CKEDITOR.tools.style.parse._colors}))}}},array:{filter:function(a,e,g){var b=[];this.forEach(a,function(h,c){e.call(g,h,c,a)&&b.push(h)});return b},find:function(a,e,g){for(var b=a.length,h=0;hCKEDITOR.env.version)for(h=0;hCKEDITOR.env.version&&(this.type==CKEDITOR.NODE_ELEMENT||this.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT)&&c(d);return d},hasPrevious:function(){return!!this.$.previousSibling},hasNext:function(){return!!this.$.nextSibling},insertAfter:function(a){a.$.parentNode.insertBefore(this.$,a.$.nextSibling);return a},insertBefore:function(a){a.$.parentNode.insertBefore(this.$, a.$);return a},insertBeforeMe:function(a){this.$.parentNode.insertBefore(a.$,this.$);return a},getAddress:function(a){for(var f=[],b=this.getDocument().$.documentElement,c=this.$;c&&c!=b;){var d=c.parentNode;d&&f.unshift(this.getIndex.call({$:c},a));c=d}return f},getDocument:function(){return new CKEDITOR.dom.document(this.$.ownerDocument||this.$.parentNode.ownerDocument)},getIndex:function(a){function f(a,g){var h=g?a.nextSibling:a.previousSibling;return h&&h.nodeType==CKEDITOR.NODE_TEXT?b(h)?f(h, g):h:null}function b(a){return!a.nodeValue||a.nodeValue==CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE}var c=this.$,d=-1,l;if(!this.$.parentNode||a&&c.nodeType==CKEDITOR.NODE_TEXT&&b(c)&&!f(c)&&!f(c,!0))return-1;do a&&c!=this.$&&c.nodeType==CKEDITOR.NODE_TEXT&&(l||b(c))||(d++,l=c.nodeType==CKEDITOR.NODE_TEXT);while(c=c.previousSibling);return d},getNextSourceNode:function(a,f,b){if(b&&!b.call){var c=b;b=function(a){return!a.equals(c)}}a=!a&&this.getFirst&&this.getFirst();var d;if(!a){if(this.type== CKEDITOR.NODE_ELEMENT&&b&&!1===b(this,!0))return null;a=this.getNext()}for(;!a&&(d=(d||this).getParent());){if(b&&!1===b(d,!0))return null;a=d.getNext()}return!a||b&&!1===b(a)?null:f&&f!=a.type?a.getNextSourceNode(!1,f,b):a},getPreviousSourceNode:function(a,f,b){if(b&&!b.call){var c=b;b=function(a){return!a.equals(c)}}a=!a&&this.getLast&&this.getLast();var d;if(!a){if(this.type==CKEDITOR.NODE_ELEMENT&&b&&!1===b(this,!0))return null;a=this.getPrevious()}for(;!a&&(d=(d||this).getParent());){if(b&&!1=== b(d,!0))return null;a=d.getPrevious()}return!a||b&&!1===b(a)?null:f&&a.type!=f?a.getPreviousSourceNode(!1,f,b):a},getPrevious:function(a){var f=this.$,b;do b=(f=f.previousSibling)&&10!=f.nodeType&&new CKEDITOR.dom.node(f);while(b&&a&&!a(b));return b},getNext:function(a){var f=this.$,b;do b=(f=f.nextSibling)&&new CKEDITOR.dom.node(f);while(b&&a&&!a(b));return b},getParent:function(a){var f=this.$.parentNode;return f&&(f.nodeType==CKEDITOR.NODE_ELEMENT||a&&f.nodeType==CKEDITOR.NODE_DOCUMENT_FRAGMENT)? new CKEDITOR.dom.node(f):null},getParents:function(a){var f=this,b=[];do b[a?"push":"unshift"](f);while(f=f.getParent());return b},getCommonAncestor:function(a){if(a.equals(this))return this;if(a.contains&&a.contains(this))return a;var f=this.contains?this:this.getParent();do if(f.contains(a))return f;while(f=f.getParent());return null},getPosition:function(a){var f=this.$,b=a.$;if(f.compareDocumentPosition)return f.compareDocumentPosition(b);if(f==b)return CKEDITOR.POSITION_IDENTICAL;if(this.type== CKEDITOR.NODE_ELEMENT&&a.type==CKEDITOR.NODE_ELEMENT){if(f.contains){if(f.contains(b))return CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_PRECEDING;if(b.contains(f))return CKEDITOR.POSITION_IS_CONTAINED+CKEDITOR.POSITION_FOLLOWING}if("sourceIndex"in f)return 0>f.sourceIndex||0>b.sourceIndex?CKEDITOR.POSITION_DISCONNECTED:f.sourceIndex=document.documentMode||!f||(a=f+":"+a);return new CKEDITOR.dom.nodeList(this.$.getElementsByTagName(a))},getHead:function(){var a=this.$.getElementsByTagName("head")[0];return a=a?new CKEDITOR.dom.element(a):this.getDocumentElement().append(new CKEDITOR.dom.element("head"),!0)},getBody:function(){return new CKEDITOR.dom.element(this.$.body)}, getDocumentElement:function(){return new CKEDITOR.dom.element(this.$.documentElement)},getWindow:function(){return new CKEDITOR.dom.window(this.$.parentWindow||this.$.defaultView)},write:function(a){this.$.open("text/html","replace");CKEDITOR.env.ie&&(a=a.replace(/(?:^\s*]*?>)|^/i,'$\x26\n\x3cscript data-cke-temp\x3d"1"\x3e('+CKEDITOR.tools.fixDomain+")();\x3c/script\x3e"));this.$.write(a);this.$.close()},find:function(a){return new CKEDITOR.dom.nodeList(this.$.querySelectorAll(a))},findOne:function(a){return(a= this.$.querySelector(a))?new CKEDITOR.dom.element(a):null},_getHtml5ShivFrag:function(){var a=this.getCustomData("html5ShivFrag");a||(a=this.$.createDocumentFragment(),CKEDITOR.tools.enableHtml5Elements(a,!0),this.setCustomData("html5ShivFrag",a));return a}}),CKEDITOR.dom.nodeList=function(a){this.$=a},CKEDITOR.dom.nodeList.prototype={count:function(){return this.$.length},getItem:function(a){return 0>a||a>=this.$.length?null:(a=this.$[a])?new CKEDITOR.dom.node(a):null},toArray:function(){return CKEDITOR.tools.array.map(this.$, function(a){return new CKEDITOR.dom.node(a)})}},CKEDITOR.dom.element=function(a,f){"string"==typeof a&&(a=(f?f.$:document).createElement(a));CKEDITOR.dom.domObject.call(this,a)},CKEDITOR.dom.element.get=function(a){return(a="string"==typeof a?document.getElementById(a)||document.getElementsByName(a)[0]:a)&&(a.$?a:new CKEDITOR.dom.element(a))},CKEDITOR.dom.element.prototype=new CKEDITOR.dom.node,CKEDITOR.dom.element.createFromHtml=function(a,f){var b=new CKEDITOR.dom.element("div",f);b.setHtml(a); return b.getFirst().remove()},CKEDITOR.dom.element.setMarker=function(a,f,b,c){var d=f.getCustomData("list_marker_id")||f.setCustomData("list_marker_id",CKEDITOR.tools.getNextNumber()).getCustomData("list_marker_id"),l=f.getCustomData("list_marker_names")||f.setCustomData("list_marker_names",{}).getCustomData("list_marker_names");a[d]=f;l[b]=1;return f.setCustomData(b,c)},CKEDITOR.dom.element.clearAllMarkers=function(a){for(var f in a)CKEDITOR.dom.element.clearMarkers(a,a[f],1)},CKEDITOR.dom.element.clearMarkers= function(a,f,b){var c=f.getCustomData("list_marker_names"),d=f.getCustomData("list_marker_id"),l;for(l in c)f.removeCustomData(l);f.removeCustomData("list_marker_names");b&&(f.removeCustomData("list_marker_id"),delete a[d])},function(){function a(a,b){return-1<(" "+a+" ").replace(l," ").indexOf(" "+b+" ")}function f(a){var b=!0;a.$.id||(a.$.id="cke_tmp_"+CKEDITOR.tools.getNextNumber(),b=!1);return function(){b||a.removeAttribute("id")}}function b(a,b){var c=CKEDITOR.tools.escapeCss(a.$.id);return"#"+ c+" "+b.split(/,\s*/).join(", #"+c+" ")}function c(a){for(var b=0,c=0,e=k[a].length;cCKEDITOR.env.version?this.$.text+=a:this.append(new CKEDITOR.dom.text(a))},appendBogus:function(a){if(a||CKEDITOR.env.needsBrFiller){for(a=this.getLast();a&&a.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.rtrim(a.getText());)a=a.getPrevious();a&& a.is&&a.is("br")||(a=this.getDocument().createElement("br"),CKEDITOR.env.gecko&&a.setAttribute("type","_moz"),this.append(a))}},breakParent:function(a,b){var c=new CKEDITOR.dom.range(this.getDocument());c.setStartAfter(this);c.setEndAfter(a);var e=c.extractContents(!1,b||!1),d;c.insertNode(this.remove());if(CKEDITOR.env.ie&&!CKEDITOR.env.edge){for(c=new CKEDITOR.dom.element("div");d=e.getFirst();)d.$.style.backgroundColor&&(d.$.style.backgroundColor=d.$.style.backgroundColor),c.append(d);c.insertAfter(this); c.remove(!0)}else e.insertAfterNode(this)},contains:document.compareDocumentPosition?function(a){return!!(this.$.compareDocumentPosition(a.$)&16)}:function(a){var b=this.$;return a.type!=CKEDITOR.NODE_ELEMENT?b.contains(a.getParent().$):b!=a.$&&b.contains(a.$)},focus:function(){function a(){try{this.$.focus()}catch(b){}}return function(b){b?CKEDITOR.tools.setTimeout(a,100,this):a.call(this)}}(),getHtml:function(){var a=this.$.innerHTML;return CKEDITOR.env.ie?a.replace(/<\?[^>]*>/g,""):a},getOuterHtml:function(){if(this.$.outerHTML)return this.$.outerHTML.replace(/<\?[^>]*>/, "");var a=this.$.ownerDocument.createElement("div");a.appendChild(this.$.cloneNode(!0));return a.innerHTML},getClientRect:function(a){var b=CKEDITOR.tools.extend({},this.$.getBoundingClientRect());!b.width&&(b.width=b.right-b.left);!b.height&&(b.height=b.bottom-b.top);return a?CKEDITOR.tools.getAbsoluteRectPosition(this.getWindow(),b):b},setHtml:CKEDITOR.env.ie&&9>CKEDITOR.env.version?function(a){try{var b=this.$;if(this.getParent())return b.innerHTML=a;var c=this.getDocument()._getHtml5ShivFrag(); c.appendChild(b);b.innerHTML=a;c.removeChild(b);return a}catch(e){this.$.innerHTML="";b=new CKEDITOR.dom.element("body",this.getDocument());b.$.innerHTML=a;for(b=b.getChildren();b.count();)this.append(b.getItem(0));return a}}:function(a){return this.$.innerHTML=a},setText:function(){var a=document.createElement("p");a.innerHTML="x";a=a.textContent;return function(b){this.$[a?"textContent":"innerText"]=b}}(),getAttribute:function(){var a=function(a){return this.$.getAttribute(a,2)};return CKEDITOR.env.ie&& (CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)?function(a){switch(a){case "class":a="className";break;case "http-equiv":a="httpEquiv";break;case "name":return this.$.name;case "tabindex":return a=this.$.getAttribute(a,2),0!==a&&0===this.$.tabIndex&&(a=null),a;case "checked":return a=this.$.attributes.getNamedItem(a),(a.specified?a.nodeValue:this.$.checked)?"checked":null;case "hspace":case "value":return this.$[a];case "style":return this.$.style.cssText;case "contenteditable":case "contentEditable":return this.$.attributes.getNamedItem("contentEditable").specified? this.$.getAttribute("contentEditable"):null}return this.$.getAttribute(a,2)}:a}(),getAttributes:function(a){var b={},c=this.$.attributes,e;a=CKEDITOR.tools.isArray(a)?a:[];for(e=0;e=document.documentMode){var b=this.$.scopeName;"HTML"!=b&&(a=b.toLowerCase()+":"+a)}this.getName=function(){return a};return this.getName()},getValue:function(){return this.$.value},getFirst:function(a){var b=this.$.firstChild;(b=b&&new CKEDITOR.dom.node(b))&&a&&!a(b)&&(b=b.getNext(a));return b},getLast:function(a){var b=this.$.lastChild; (b=b&&new CKEDITOR.dom.node(b))&&a&&!a(b)&&(b=b.getPrevious(a));return b},getStyle:function(a){return this.$.style[CKEDITOR.tools.cssStyleToDomStyle(a)]},is:function(){var a=this.getName();if("object"==typeof arguments[0])return!!arguments[0][a];for(var b=0;bCKEDITOR.env.version&&this.is("a")){var c=this.getParent();c.type==CKEDITOR.NODE_ELEMENT&&(c=c.clone(),c.setHtml(b),b=c.getHtml(),c.setHtml(a),a=c.getHtml())}return b==a},isVisible:function(){var a=(this.$.offsetHeight||this.$.offsetWidth)&&"hidden"!=this.getComputedStyle("visibility"),b,c;a&&CKEDITOR.env.webkit&&(b=this.getWindow(),!b.equals(CKEDITOR.document.getWindow())&& (c=b.$.frameElement)&&(a=(new CKEDITOR.dom.element(c)).isVisible()));return!!a},isEmptyInlineRemoveable:function(){if(!CKEDITOR.dtd.$removeEmpty[this.getName()])return!1;for(var a=this.getChildren(),b=0,c=a.count();bCKEDITOR.env.version?function(b){return"name"==b?!!this.$.name:a.call(this,b)}:a:function(a){return!!this.$.attributes.getNamedItem(a)}}(),hide:function(){this.setStyle("display","none")},moveChildren:function(a,b){var c=this.$;a=a.$;if(c!=a){var e;if(b)for(;e=c.lastChild;)a.insertBefore(c.removeChild(e), a.firstChild);else for(;e=c.firstChild;)a.appendChild(c.removeChild(e))}},mergeSiblings:function(){function a(b,g,e){if(g&&g.type==CKEDITOR.NODE_ELEMENT){for(var c=[];g.data("cke-bookmark")||g.isEmptyInlineRemoveable();)if(c.push(g),g=e?g.getNext():g.getPrevious(),!g||g.type!=CKEDITOR.NODE_ELEMENT)return;if(b.isIdentical(g)){for(var d=e?b.getLast():b.getFirst();c.length;)c.shift().move(b,!e);g.moveChildren(b,!e);g.remove();d&&d.type==CKEDITOR.NODE_ELEMENT&&d.mergeSiblings()}}}return function(b){if(!1=== b||CKEDITOR.dtd.$removeEmpty[this.getName()]||this.is("a"))a(this,this.getNext(),!0),a(this,this.getPrevious())}}(),show:function(){this.setStyles({display:"",visibility:""})},setAttribute:function(){var a=function(a,b){this.$.setAttribute(a,b);return this};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)?function(b,c){"class"==b?this.$.className=c:"style"==b?this.$.style.cssText=c:"tabindex"==b?this.$.tabIndex=c:"checked"==b?this.$.checked=c:"contenteditable"==b?a.call(this, "contentEditable",c):a.apply(this,arguments);return this}:CKEDITOR.env.ie8Compat&&CKEDITOR.env.secure?function(b,c){if("src"==b&&c.match(/^http:\/\//))try{a.apply(this,arguments)}catch(e){}else a.apply(this,arguments);return this}:a}(),setAttributes:function(a){for(var b in a)this.setAttribute(b,a[b]);return this},setValue:function(a){this.$.value=a;return this},removeAttribute:function(){var a=function(a){this.$.removeAttribute(a)};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)? function(a){"class"==a?a="className":"tabindex"==a?a="tabIndex":"contenteditable"==a&&(a="contentEditable");this.$.removeAttribute(a)}:a}(),removeAttributes:function(a){if(CKEDITOR.tools.isArray(a))for(var b=0;bCKEDITOR.env.version?(a=Math.round(100*a),this.setStyle("filter",100<=a?"":"progid:DXImageTransform.Microsoft.Alpha(opacity\x3d"+a+")")):this.setStyle("opacity",a)},unselectable:function(){this.setStyles(CKEDITOR.tools.cssVendorPrefix("user-select","none"));if(CKEDITOR.env.ie){this.setAttribute("unselectable","on");for(var a,b=this.getElementsByTag("*"),c=0,e=b.count();cl||0l?l:d);c&&(0>f||0f?f:e,0)},setState:function(a,b,c){b=b||"cke";switch(a){case CKEDITOR.TRISTATE_ON:this.addClass(b+"_on");this.removeClass(b+"_off");this.removeClass(b+"_disabled");c&&this.setAttribute("aria-pressed",!0);c&&this.removeAttribute("aria-disabled");break;case CKEDITOR.TRISTATE_DISABLED:this.addClass(b+"_disabled");this.removeClass(b+"_off");this.removeClass(b+"_on");c&&this.setAttribute("aria-disabled", !0);c&&this.removeAttribute("aria-pressed");break;default:this.addClass(b+"_off"),this.removeClass(b+"_on"),this.removeClass(b+"_disabled"),c&&this.removeAttribute("aria-pressed"),c&&this.removeAttribute("aria-disabled")}},getFrameDocument:function(){var a=this.$;try{a.contentWindow.document}catch(b){a.src=a.src}return a&&new CKEDITOR.dom.document(a.contentWindow.document)},copyAttributes:function(a,b){var c=this.$.attributes;b=b||{};for(var e=0;e=A.getChildCount()?(A=A.getChild(C-1),F=!0):A=A.getChild(C):J=F=!0;x.type==CKEDITOR.NODE_TEXT?t?E=!0:x.split(B):0ha)for(;Z;)Z=h(Z,K,!0);K=W}t||m()}}function b(){var a=!1,b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(!0),d=CKEDITOR.dom.walker.bogus();return function(g){return c(g)||b(g)?!0:d(g)&&!a?a=!0:g.type==CKEDITOR.NODE_TEXT&&(g.hasAscendant("pre")||CKEDITOR.tools.trim(g.getText()).length)||g.type==CKEDITOR.NODE_ELEMENT&&!g.is(l)?!1:!0}}function c(a){var b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(1); return function(d){return c(d)||b(d)?!0:!a&&k(d)||d.type==CKEDITOR.NODE_ELEMENT&&d.is(CKEDITOR.dtd.$removeEmpty)}}function d(a){return function(){var b;return this[a?"getPreviousNode":"getNextNode"](function(a){!b&&m(a)&&(b=a);return h(a)&&!(k(a)&&a.equals(b))})}}var l={abbr:1,acronym:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,"var":1},k=CKEDITOR.dom.walker.bogus(),g=/^[\t\r\n ]*(?: |\xa0)$/, h=CKEDITOR.dom.walker.editable(),m=CKEDITOR.dom.walker.ignored(!0);CKEDITOR.dom.range.prototype={clone:function(){var a=new CKEDITOR.dom.range(this.root);a._setStartContainer(this.startContainer);a.startOffset=this.startOffset;a._setEndContainer(this.endContainer);a.endOffset=this.endOffset;a.collapsed=this.collapsed;return a},collapse:function(a){a?(this._setEndContainer(this.startContainer),this.endOffset=this.startOffset):(this._setStartContainer(this.endContainer),this.startOffset=this.endOffset); this.collapsed=!0},cloneContents:function(a){var b=new CKEDITOR.dom.documentFragment(this.document);this.collapsed||f(this,2,b,!1,"undefined"==typeof a?!0:a);return b},deleteContents:function(a){this.collapsed||f(this,0,null,a)},extractContents:function(a,b){var c=new CKEDITOR.dom.documentFragment(this.document);this.collapsed||f(this,1,c,a,"undefined"==typeof b?!0:b);return c},createBookmark:function(a){var b,c,d,g,h=this.collapsed;b=this.document.createElement("span");b.data("cke-bookmark",1);b.setStyle("display", "none");b.setHtml("\x26nbsp;");a&&(d="cke_bm_"+CKEDITOR.tools.getNextNumber(),b.setAttribute("id",d+(h?"C":"S")));h||(c=b.clone(),c.setHtml("\x26nbsp;"),a&&c.setAttribute("id",d+"E"),g=this.clone(),g.collapse(),g.insertNode(c));g=this.clone();g.collapse(!0);g.insertNode(b);c?(this.setStartAfter(b),this.setEndBefore(c)):this.moveToPosition(b,CKEDITOR.POSITION_AFTER_END);return{startNode:a?d+(h?"C":"S"):b,endNode:a?d+"E":c,serializable:a,collapsed:h}},createBookmark2:function(){function a(b){var e= b.container,d=b.offset,g;g=e;var h=d;g=g.type!=CKEDITOR.NODE_ELEMENT||0===h||h==g.getChildCount()?0:g.getChild(h-1).type==CKEDITOR.NODE_TEXT&&g.getChild(h).type==CKEDITOR.NODE_TEXT;g&&(e=e.getChild(d-1),d=e.getLength());if(e.type==CKEDITOR.NODE_ELEMENT&&0=a.offset&&(a.offset=d.getIndex(),a.container=d.getParent()))}}var c=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_TEXT,!0);return function(c){var d=this.collapsed,g={container:this.startContainer, offset:this.startOffset},h={container:this.endContainer,offset:this.endOffset};c&&(a(g),b(g,this.root),d||(a(h),b(h,this.root)));return{start:g.container.getAddress(c),end:d?null:h.container.getAddress(c),startOffset:g.offset,endOffset:h.offset,normalized:c,collapsed:d,is2:!0}}}(),moveToBookmark:function(a){if(a.is2){var b=this.document.getByAddress(a.start,a.normalized),c=a.startOffset,d=a.end&&this.document.getByAddress(a.end,a.normalized);a=a.endOffset;this.setStart(b,c);d?this.setEnd(d,a):this.collapse(!0)}else b= (c=a.serializable)?this.document.getById(a.startNode):a.startNode,a=c?this.document.getById(a.endNode):a.endNode,this.setStartBefore(b),b.remove(),a?(this.setEndBefore(a),a.remove()):this.collapse(!0)},getBoundaryNodes:function(){var a=this.startContainer,b=this.endContainer,c=this.startOffset,d=this.endOffset,g;if(a.type==CKEDITOR.NODE_ELEMENT)if(g=a.getChildCount(),g>c)a=a.getChild(c);else if(1>g)a=a.getPreviousSourceNode();else{for(a=a.$;a.lastChild;)a=a.lastChild;a=new CKEDITOR.dom.node(a);a= a.getNextSourceNode()||a}if(b.type==CKEDITOR.NODE_ELEMENT)if(g=b.getChildCount(),g>d)b=b.getChild(d).getPreviousSourceNode(!0);else if(1>g)b=b.getPreviousSourceNode();else{for(b=b.$;b.lastChild;)b=b.lastChild;b=new CKEDITOR.dom.node(b)}a.getPosition(b)&CKEDITOR.POSITION_FOLLOWING&&(a=b);return{startNode:a,endNode:b}},getCommonAncestor:function(a,b){var c=this.startContainer,d=this.endContainer,c=c.equals(d)?a&&c.type==CKEDITOR.NODE_ELEMENT&&this.startOffset==this.endOffset-1?c.getChild(this.startOffset): c:c.getCommonAncestor(d);return b&&!c.is?c.getParent():c},optimize:function(){var a=this.startContainer,b=this.startOffset;a.type!=CKEDITOR.NODE_ELEMENT&&(b?b>=a.getLength()&&this.setStartAfter(a):this.setStartBefore(a));a=this.endContainer;b=this.endOffset;a.type!=CKEDITOR.NODE_ELEMENT&&(b?b>=a.getLength()&&this.setEndAfter(a):this.setEndBefore(a))},optimizeBookmark:function(){var a=this.startContainer,b=this.endContainer;a.is&&a.is("span")&&a.data("cke-bookmark")&&this.setStartAt(a,CKEDITOR.POSITION_BEFORE_START); b&&b.is&&b.is("span")&&b.data("cke-bookmark")&&this.setEndAt(b,CKEDITOR.POSITION_AFTER_END)},trim:function(a,b){var c=this.startContainer,d=this.startOffset,g=this.collapsed;if((!a||g)&&c&&c.type==CKEDITOR.NODE_TEXT){if(d)if(d>=c.getLength())d=c.getIndex()+1,c=c.getParent();else{var h=c.split(d),d=c.getIndex()+1,c=c.getParent();this.startContainer.equals(this.endContainer)?this.setEnd(h,this.endOffset-this.startOffset):c.equals(this.endContainer)&&(this.endOffset+=1)}else d=c.getIndex(),c=c.getParent(); this.setStart(c,d);if(g){this.collapse(!0);return}}c=this.endContainer;d=this.endOffset;b||g||!c||c.type!=CKEDITOR.NODE_TEXT||(d?(d>=c.getLength()||c.split(d),d=c.getIndex()+1):d=c.getIndex(),c=c.getParent(),this.setEnd(c,d))},enlarge:function(a,b){function c(a){return a&&a.type==CKEDITOR.NODE_ELEMENT&&a.hasAttribute("contenteditable")?null:a}var d=new RegExp(/[^\s\ufeff]/);switch(a){case CKEDITOR.ENLARGE_INLINE:var g=1;case CKEDITOR.ENLARGE_ELEMENT:var h=function(a,b){var e=new CKEDITOR.dom.range(m); e.setStart(a,b);e.setEndAt(m,CKEDITOR.POSITION_BEFORE_END);var e=new CKEDITOR.dom.walker(e),c;for(e.guard=function(a){return!(a.type==CKEDITOR.NODE_ELEMENT&&a.isBlockBoundary())};c=e.next();){if(c.type!=CKEDITOR.NODE_TEXT)return!1;H=c!=a?c.getText():c.substring(b);if(d.test(H))return!1}return!0};if(this.collapsed)break;var f=this.getCommonAncestor(),m=this.root,l,k,t,x,A,B=!1,C,H;C=this.startContainer;var F=this.startOffset;C.type==CKEDITOR.NODE_TEXT?(F&&(C=!CKEDITOR.tools.trim(C.substring(0,F)).length&& C,B=!!C),C&&((x=C.getPrevious())||(t=C.getParent()))):(F&&(x=C.getChild(F-1)||C.getLast()),x||(t=C));for(t=c(t);t||x;){if(t&&!x){!A&&t.equals(f)&&(A=!0);if(g?t.isBlockBoundary():!m.contains(t))break;B&&"inline"==t.getComputedStyle("display")||(B=!1,A?l=t:this.setStartBefore(t));x=t.getPrevious()}for(;x;)if(C=!1,x.type==CKEDITOR.NODE_COMMENT)x=x.getPrevious();else{if(x.type==CKEDITOR.NODE_TEXT)H=x.getText(),d.test(H)&&(x=null),C=/[\s\ufeff]$/.test(H);else if((x.$.offsetWidth>(CKEDITOR.env.webkit?1: 0)||b&&x.is("br"))&&!x.data("cke-bookmark"))if(B&&CKEDITOR.dtd.$removeEmpty[x.getName()]){H=x.getText();if(d.test(H))x=null;else for(var F=x.$.getElementsByTagName("*"),I=0,J;J=F[I++];)if(!CKEDITOR.dtd.$removeEmpty[J.nodeName.toLowerCase()]){x=null;break}x&&(C=!!H.length)}else x=null;C&&(B?A?l=t:t&&this.setStartBefore(t):B=!0);if(x){C=x.getPrevious();if(!t&&!C){t=x;x=null;break}x=C}else t=null}t&&(t=c(t.getParent()))}C=this.endContainer;F=this.endOffset;t=x=null;A=B=!1;C.type==CKEDITOR.NODE_TEXT? CKEDITOR.tools.trim(C.substring(F)).length?B=!0:(B=!C.getLength(),F==C.getLength()?(x=C.getNext())||(t=C.getParent()):h(C,F)&&(t=C.getParent())):(x=C.getChild(F))||(t=C);for(;t||x;){if(t&&!x){!A&&t.equals(f)&&(A=!0);if(g?t.isBlockBoundary():!m.contains(t))break;B&&"inline"==t.getComputedStyle("display")||(B=!1,A?k=t:t&&this.setEndAfter(t));x=t.getNext()}for(;x;){C=!1;if(x.type==CKEDITOR.NODE_TEXT)H=x.getText(),h(x,0)||(x=null),C=/^[\s\ufeff]/.test(H);else if(x.type==CKEDITOR.NODE_ELEMENT){if((0=f.getLength()?h.setStartAfter(f):(h.setStartBefore(f),c=0):h.setStartBefore(f));m&&m.type==CKEDITOR.NODE_TEXT&&(k?k>=m.getLength()?h.setEndAfter(m):(h.setEndAfter(m),t=0):h.setEndBefore(m));var h=new CKEDITOR.dom.walker(h),x=CKEDITOR.dom.walker.bookmark(),A=CKEDITOR.dom.walker.bogus();h.evaluator=function(b){return b.type==(a==CKEDITOR.SHRINK_ELEMENT?CKEDITOR.NODE_ELEMENT:CKEDITOR.NODE_TEXT)}; var B;h.guard=function(b,c){if(g&&A(b)||x(b))return!0;if(a==CKEDITOR.SHRINK_ELEMENT&&b.type==CKEDITOR.NODE_TEXT||c&&b.equals(B)||!1===d&&b.type==CKEDITOR.NODE_ELEMENT&&b.isBlockBoundary()||b.type==CKEDITOR.NODE_ELEMENT&&b.hasAttribute("contenteditable"))return!1;c||b.type!=CKEDITOR.NODE_ELEMENT||(B=b);return!0};c&&(f=h[a==CKEDITOR.SHRINK_ELEMENT?"lastForward":"next"]())&&this.setStartAt(f,b?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_START);t&&(h.reset(),(h=h[a==CKEDITOR.SHRINK_ELEMENT? "lastBackward":"previous"]())&&this.setEndAt(h,b?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_AFTER_END));return!(!c&&!t)}},insertNode:function(a){this.optimizeBookmark();this.trim(!1,!0);var b=this.startContainer,c=b.getChild(this.startOffset);c?a.insertBefore(c):b.append(a);a.getParent()&&a.getParent().equals(this.endContainer)&&this.endOffset++;this.setStartBefore(a)},moveToPosition:function(a,b){this.setStartAt(a,b);this.collapse(!0)},moveToRange:function(a){this.setStart(a.startContainer,a.startOffset); this.setEnd(a.endContainer,a.endOffset)},selectNodeContents:function(a){this.setStart(a,0);this.setEnd(a,a.type==CKEDITOR.NODE_TEXT?a.getLength():a.getChildCount())},setStart:function(b,c){b.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$empty[b.getName()]&&(c=b.getIndex(),b=b.getParent());this._setStartContainer(b);this.startOffset=c;this.endContainer||(this._setEndContainer(b),this.endOffset=c);a(this)},setEnd:function(b,c){b.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$empty[b.getName()]&&(c=b.getIndex()+ 1,b=b.getParent());this._setEndContainer(b);this.endOffset=c;this.startContainer||(this._setStartContainer(b),this.startOffset=c);a(this)},setStartAfter:function(a){this.setStart(a.getParent(),a.getIndex()+1)},setStartBefore:function(a){this.setStart(a.getParent(),a.getIndex())},setEndAfter:function(a){this.setEnd(a.getParent(),a.getIndex()+1)},setEndBefore:function(a){this.setEnd(a.getParent(),a.getIndex())},setStartAt:function(b,c){switch(c){case CKEDITOR.POSITION_AFTER_START:this.setStart(b,0); break;case CKEDITOR.POSITION_BEFORE_END:b.type==CKEDITOR.NODE_TEXT?this.setStart(b,b.getLength()):this.setStart(b,b.getChildCount());break;case CKEDITOR.POSITION_BEFORE_START:this.setStartBefore(b);break;case CKEDITOR.POSITION_AFTER_END:this.setStartAfter(b)}a(this)},setEndAt:function(b,c){switch(c){case CKEDITOR.POSITION_AFTER_START:this.setEnd(b,0);break;case CKEDITOR.POSITION_BEFORE_END:b.type==CKEDITOR.NODE_TEXT?this.setEnd(b,b.getLength()):this.setEnd(b,b.getChildCount());break;case CKEDITOR.POSITION_BEFORE_START:this.setEndBefore(b); break;case CKEDITOR.POSITION_AFTER_END:this.setEndAfter(b)}a(this)},fixBlock:function(a,b){var c=this.createBookmark(),d=this.document.createElement(b);this.collapse(a);this.enlarge(CKEDITOR.ENLARGE_BLOCK_CONTENTS);this.extractContents().appendTo(d);d.trim();this.insertNode(d);var g=d.getBogus();g&&g.remove();d.appendBogus();this.moveToBookmark(c);return d},splitBlock:function(a,b){var c=new CKEDITOR.dom.elementPath(this.startContainer,this.root),d=new CKEDITOR.dom.elementPath(this.endContainer,this.root), g=c.block,h=d.block,f=null;if(!c.blockLimit.equals(d.blockLimit))return null;"br"!=a&&(g||(g=this.fixBlock(!0,a),h=(new CKEDITOR.dom.elementPath(this.endContainer,this.root)).block),h||(h=this.fixBlock(!1,a)));c=g&&this.checkStartOfBlock();d=h&&this.checkEndOfBlock();this.deleteContents();g&&g.equals(h)&&(d?(f=new CKEDITOR.dom.elementPath(this.startContainer,this.root),this.moveToPosition(h,CKEDITOR.POSITION_AFTER_END),h=null):c?(f=new CKEDITOR.dom.elementPath(this.startContainer,this.root),this.moveToPosition(g, CKEDITOR.POSITION_BEFORE_START),g=null):(h=this.splitElement(g,b||!1),g.is("ul","ol")||g.appendBogus()));return{previousBlock:g,nextBlock:h,wasStartOfBlock:c,wasEndOfBlock:d,elementPath:f}},splitElement:function(a,b){if(!this.collapsed)return null;this.setEndAt(a,CKEDITOR.POSITION_BEFORE_END);var c=this.extractContents(!1,b||!1),d=a.clone(!1,b||!1);c.appendTo(d);d.insertAfter(a);this.moveToPosition(a,CKEDITOR.POSITION_AFTER_END);return d},removeEmptyBlocksAtEnd:function(){function a(e){return function(a){return b(a)|| c(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.isEmptyInlineRemoveable()||e.is("table")&&a.is("caption")?!1:!0}}var b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(!1);return function(b){for(var c=this.createBookmark(),d=this[b?"endPath":"startPath"](),g=d.block||d.blockLimit,h;g&&!g.equals(d.root)&&!g.getFirst(a(g));)h=g.getParent(),this[b?"setEndAt":"setStartAt"](g,CKEDITOR.POSITION_AFTER_END),g.remove(1),g=h;this.moveToBookmark(c)}}(),startPath:function(){return new CKEDITOR.dom.elementPath(this.startContainer, this.root)},endPath:function(){return new CKEDITOR.dom.elementPath(this.endContainer,this.root)},checkBoundaryOfElement:function(a,b){var d=b==CKEDITOR.START,g=this.clone();g.collapse(d);g[d?"setStartAt":"setEndAt"](a,d?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END);g=new CKEDITOR.dom.walker(g);g.evaluator=c(d);return g[d?"checkBackward":"checkForward"]()},checkStartOfBlock:function(){var a=this.startContainer,c=this.startOffset;CKEDITOR.env.ie&&c&&a.type==CKEDITOR.NODE_TEXT&&(a=CKEDITOR.tools.ltrim(a.substring(0, c)),g.test(a)&&this.trim(0,1));this.trim();a=new CKEDITOR.dom.elementPath(this.startContainer,this.root);c=this.clone();c.collapse(!0);c.setStartAt(a.block||a.blockLimit,CKEDITOR.POSITION_AFTER_START);a=new CKEDITOR.dom.walker(c);a.evaluator=b();return a.checkBackward()},checkEndOfBlock:function(){var a=this.endContainer,c=this.endOffset;CKEDITOR.env.ie&&a.type==CKEDITOR.NODE_TEXT&&(a=CKEDITOR.tools.rtrim(a.substring(c)),g.test(a)&&this.trim(1,0));this.trim();a=new CKEDITOR.dom.elementPath(this.endContainer, this.root);c=this.clone();c.collapse(!1);c.setEndAt(a.block||a.blockLimit,CKEDITOR.POSITION_BEFORE_END);a=new CKEDITOR.dom.walker(c);a.evaluator=b();return a.checkForward()},getPreviousNode:function(a,b,c){var d=this.clone();d.collapse(1);d.setStartAt(c||this.root,CKEDITOR.POSITION_AFTER_START);c=new CKEDITOR.dom.walker(d);c.evaluator=a;c.guard=b;return c.previous()},getNextNode:function(a,b,c){var d=this.clone();d.collapse();d.setEndAt(c||this.root,CKEDITOR.POSITION_BEFORE_END);c=new CKEDITOR.dom.walker(d); c.evaluator=a;c.guard=b;return c.next()},checkReadOnly:function(){function a(b,c){for(;b;){if(b.type==CKEDITOR.NODE_ELEMENT){if("false"==b.getAttribute("contentEditable")&&!b.data("cke-editable"))return 0;if(b.is("html")||"true"==b.getAttribute("contentEditable")&&(b.contains(c)||b.equals(c)))break}b=b.getParent()}return 1}return function(){var b=this.startContainer,c=this.endContainer;return!(a(b,c)&&a(c,b))}}(),moveToElementEditablePosition:function(a,b){if(a.type==CKEDITOR.NODE_ELEMENT&&!a.isEditable(!1))return this.moveToPosition(a, b?CKEDITOR.POSITION_AFTER_END:CKEDITOR.POSITION_BEFORE_START),!0;for(var c=0;a;){if(a.type==CKEDITOR.NODE_TEXT){b&&this.endContainer&&this.checkEndOfBlock()&&g.test(a.getText())?this.moveToPosition(a,CKEDITOR.POSITION_BEFORE_START):this.moveToPosition(a,b?CKEDITOR.POSITION_AFTER_END:CKEDITOR.POSITION_BEFORE_START);c=1;break}if(a.type==CKEDITOR.NODE_ELEMENT)if(a.isEditable())this.moveToPosition(a,b?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_AFTER_START),c=1;else if(b&&a.is("br")&&this.endContainer&& this.checkEndOfBlock())this.moveToPosition(a,CKEDITOR.POSITION_BEFORE_START);else if("false"==a.getAttribute("contenteditable")&&a.is(CKEDITOR.dtd.$block))return this.setStartBefore(a),this.setEndAfter(a),!0;var d=a,h=c,f=void 0;d.type==CKEDITOR.NODE_ELEMENT&&d.isEditable(!1)&&(f=d[b?"getLast":"getFirst"](m));h||f||(f=d[b?"getPrevious":"getNext"](m));a=f}return!!c},moveToClosestEditablePosition:function(a,b){var c,d=0,g,h,f=[CKEDITOR.POSITION_AFTER_END,CKEDITOR.POSITION_BEFORE_START];a?(c=new CKEDITOR.dom.range(this.root), c.moveToPosition(a,f[b?0:1])):c=this.clone();if(a&&!a.is(CKEDITOR.dtd.$block))d=1;else if(g=c[b?"getNextEditableNode":"getPreviousEditableNode"]())d=1,(h=g.type==CKEDITOR.NODE_ELEMENT)&&g.is(CKEDITOR.dtd.$block)&&"false"==g.getAttribute("contenteditable")?(c.setStartAt(g,CKEDITOR.POSITION_BEFORE_START),c.setEndAt(g,CKEDITOR.POSITION_AFTER_END)):!CKEDITOR.env.needsBrFiller&&h&&g.is(CKEDITOR.dom.walker.validEmptyBlockContainers)?(c.setEnd(g,0),c.collapse()):c.moveToPosition(g,f[b?1:0]);d&&this.moveToRange(c); return!!d},moveToElementEditStart:function(a){return this.moveToElementEditablePosition(a)},moveToElementEditEnd:function(a){return this.moveToElementEditablePosition(a,!0)},getEnclosedNode:function(){var a=this.clone();a.optimize();if(a.startContainer.type!=CKEDITOR.NODE_ELEMENT||a.endContainer.type!=CKEDITOR.NODE_ELEMENT)return null;var a=new CKEDITOR.dom.walker(a),b=CKEDITOR.dom.walker.bookmark(!1,!0),c=CKEDITOR.dom.walker.whitespaces(!0);a.evaluator=function(a){return c(a)&&b(a)};var d=a.next(); a.reset();return d&&d.equals(a.previous())?d:null},getTouchedStartNode:function(){var a=this.startContainer;return this.collapsed||a.type!=CKEDITOR.NODE_ELEMENT?a:a.getChild(this.startOffset)||a},getTouchedEndNode:function(){var a=this.endContainer;return this.collapsed||a.type!=CKEDITOR.NODE_ELEMENT?a:a.getChild(this.endOffset-1)||a},getNextEditableNode:d(),getPreviousEditableNode:d(1),_getTableElement:function(a){a=a||{td:1,th:1,tr:1,tbody:1,thead:1,tfoot:1,table:1};var b=this.startContainer,c= this.endContainer,d=b.getAscendant("table",!0),g=c.getAscendant("table",!0);return d&&!this.root.contains(d)?null:CKEDITOR.env.safari&&d&&c.equals(this.root)?b.getAscendant(a,!0):this.getEnclosedNode()?this.getEnclosedNode().getAscendant(a,!0):d&&g&&(d.equals(g)||d.contains(g)||g.contains(d))?b.getAscendant(a,!0):null},scrollIntoView:function(){var a=new CKEDITOR.dom.element.createFromHtml("\x3cspan\x3e\x26nbsp;\x3c/span\x3e",this.document),b,c,d,g=this.clone();g.optimize();(d=g.startContainer.type== CKEDITOR.NODE_TEXT)?(c=g.startContainer.getText(),b=g.startContainer.split(g.startOffset),a.insertAfter(g.startContainer)):g.insertNode(a);a.scrollIntoView();d&&(g.startContainer.setText(c),b.remove());a.remove()},getClientRects:function(){function a(b,c){var e=CKEDITOR.tools.array.map(b,function(a){return a}),d=new CKEDITOR.dom.range(c.root),g,h,f;c.startContainer instanceof CKEDITOR.dom.element&&(h=0===c.startOffset&&c.startContainer.hasAttribute("data-widget"));c.endContainer instanceof CKEDITOR.dom.element&& (f=(f=c.endOffset===(c.endContainer.getChildCount?c.endContainer.getChildCount():c.endContainer.length))&&c.endContainer.hasAttribute("data-widget"));h&&d.setStart(c.startContainer.getParent(),c.startContainer.getIndex());f&&d.setEnd(c.endContainer.getParent(),c.endContainer.getIndex()+1);if(h||f)c=d;d=c.cloneContents().find("[data-cke-widget-id]").toArray();if(d=CKEDITOR.tools.array.map(d,function(a){var b=c.root.editor;a=a.getAttribute("data-cke-widget-id");return b.widgets.instances[a].element}))return d= CKEDITOR.tools.array.map(d,function(a){var b;b=a.getParent().hasClass("cke_widget_wrapper")?a.getParent():a;g=this.root.getDocument().$.createRange();g.setStart(b.getParent().$,b.getIndex());g.setEnd(b.getParent().$,b.getIndex()+1);b=g.getClientRects();b.widgetRect=a.getClientRect();return b},c),CKEDITOR.tools.array.forEach(d,function(a){function b(d){CKEDITOR.tools.array.forEach(e,function(b,g){var h=CKEDITOR.tools.objectCompare(a[d],b);h||(h=CKEDITOR.tools.objectCompare(a.widgetRect,b));h&&(Array.prototype.splice.call(e, g,a.length-d,a.widgetRect),c=!0)});c||(darguments.length||(this.range=a,this.forceBrBreak=0,this.enlargeBr=1,this.enforceRealBlocks=0,this._||(this._={}))}function f(a){var b=[];a.forEach(function(a){if("true"==a.getAttribute("contenteditable"))return b.push(a),!1},CKEDITOR.NODE_ELEMENT,!0);return b}function b(a,c,d,g){a:{null==g&&(g=f(d));for(var h;h=g.shift();)if(h.getDtd().p){g={element:h,remaining:g};break a}g=null}if(!g)return 0;if((h=CKEDITOR.filter.instances[g.element.data("cke-filter")])&& !h.check(c))return b(a,c,d,g.remaining);c=new CKEDITOR.dom.range(g.element);c.selectNodeContents(g.element);c=c.createIterator();c.enlargeBr=a.enlargeBr;c.enforceRealBlocks=a.enforceRealBlocks;c.activeFilter=c.filter=h;a._.nestedEditable={element:g.element,container:d,remaining:g.remaining,iterator:c};return 1}function c(a,b,c){if(!b)return!1;a=a.clone();a.collapse(!c);return a.checkBoundaryOfElement(b,c?CKEDITOR.START:CKEDITOR.END)}var d=/^[\r\n\t ]+$/,l=CKEDITOR.dom.walker.bookmark(!1,!0),k=CKEDITOR.dom.walker.whitespaces(!0), g=function(a){return l(a)&&k(a)},h={dd:1,dt:1,li:1};a.prototype={getNextParagraph:function(a){var e,f,k,y,u;a=a||"p";if(this._.nestedEditable){if(e=this._.nestedEditable.iterator.getNextParagraph(a))return this.activeFilter=this._.nestedEditable.iterator.activeFilter,e;this.activeFilter=this.filter;if(b(this,a,this._.nestedEditable.container,this._.nestedEditable.remaining))return this.activeFilter=this._.nestedEditable.iterator.activeFilter,this._.nestedEditable.iterator.getNextParagraph(a);this._.nestedEditable= null}if(!this.range.root.getDtd()[a])return null;if(!this._.started){var p=this.range.clone();f=p.startPath();var v=p.endPath(),w=!p.collapsed&&c(p,f.block),r=!p.collapsed&&c(p,v.block,1);p.shrink(CKEDITOR.SHRINK_ELEMENT,!0);w&&p.setStartAt(f.block,CKEDITOR.POSITION_BEFORE_END);r&&p.setEndAt(v.block,CKEDITOR.POSITION_AFTER_START);f=p.endContainer.hasAscendant("pre",!0)||p.startContainer.hasAscendant("pre",!0);p.enlarge(this.forceBrBreak&&!f||!this.enlargeBr?CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:CKEDITOR.ENLARGE_BLOCK_CONTENTS); p.collapsed||(f=new CKEDITOR.dom.walker(p.clone()),v=CKEDITOR.dom.walker.bookmark(!0,!0),f.evaluator=v,this._.nextNode=f.next(),f=new CKEDITOR.dom.walker(p.clone()),f.evaluator=v,f=f.previous(),this._.lastNode=f.getNextSourceNode(!0,null,p.root),this._.lastNode&&this._.lastNode.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.trim(this._.lastNode.getText())&&this._.lastNode.getParent().isBlockBoundary()&&(v=this.range.clone(),v.moveToPosition(this._.lastNode,CKEDITOR.POSITION_AFTER_END),v.checkEndOfBlock()&& (v=new CKEDITOR.dom.elementPath(v.endContainer,v.root),this._.lastNode=(v.block||v.blockLimit).getNextSourceNode(!0))),this._.lastNode&&p.root.contains(this._.lastNode)||(this._.lastNode=this._.docEndMarker=p.document.createText(""),this._.lastNode.insertAfter(f)),p=null);this._.started=1;f=p}v=this._.nextNode;p=this._.lastNode;for(this._.nextNode=null;v;){var w=0,r=v.hasAscendant("pre"),z=v.type!=CKEDITOR.NODE_ELEMENT,t=0;if(z)v.type==CKEDITOR.NODE_TEXT&&d.test(v.getText())&&(z=0);else{var x=v.getName(); if(CKEDITOR.dtd.$block[x]&&"false"==v.getAttribute("contenteditable")){e=v;b(this,a,e);break}else if(v.isBlockBoundary(this.forceBrBreak&&!r&&{br:1})){if("br"==x)z=1;else if(!f&&!v.getChildCount()&&"hr"!=x){e=v;k=v.equals(p);break}f&&(f.setEndAt(v,CKEDITOR.POSITION_BEFORE_START),"br"!=x&&(this._.nextNode=v));w=1}else{if(v.getFirst()){f||(f=this.range.clone(),f.setStartAt(v,CKEDITOR.POSITION_BEFORE_START));v=v.getFirst();continue}z=1}}z&&!f&&(f=this.range.clone(),f.setStartAt(v,CKEDITOR.POSITION_BEFORE_START)); k=(!w||z)&&v.equals(p);if(f&&!w)for(;!v.getNext(g)&&!k;){x=v.getParent();if(x.isBlockBoundary(this.forceBrBreak&&!r&&{br:1})){w=1;z=0;k||x.equals(p);f.setEndAt(x,CKEDITOR.POSITION_BEFORE_END);break}v=x;z=1;k=v.equals(p);t=1}z&&f.setEndAt(v,CKEDITOR.POSITION_AFTER_END);v=this._getNextSourceNode(v,t,p);if((k=!v)||w&&f)break}if(!e){if(!f)return this._.docEndMarker&&this._.docEndMarker.remove(),this._.nextNode=null;e=new CKEDITOR.dom.elementPath(f.startContainer,f.root);v=e.blockLimit;w={div:1,th:1,td:1}; e=e.block;!e&&v&&!this.enforceRealBlocks&&w[v.getName()]&&f.checkStartOfBlock()&&f.checkEndOfBlock()&&!v.equals(f.root)?e=v:!e||this.enforceRealBlocks&&e.is(h)?(e=this.range.document.createElement(a),f.extractContents().appendTo(e),e.trim(),f.insertNode(e),y=u=!0):"li"!=e.getName()?f.checkStartOfBlock()&&f.checkEndOfBlock()||(e=e.clone(!1),f.extractContents().appendTo(e),e.trim(),u=f.splitBlock(),y=!u.wasStartOfBlock,u=!u.wasEndOfBlock,f.insertNode(e)):k||(this._.nextNode=e.equals(p)?null:this._getNextSourceNode(f.getBoundaryNodes().endNode, 1,p))}y&&(y=e.getPrevious())&&y.type==CKEDITOR.NODE_ELEMENT&&("br"==y.getName()?y.remove():y.getLast()&&"br"==y.getLast().$.nodeName.toLowerCase()&&y.getLast().remove());u&&(y=e.getLast())&&y.type==CKEDITOR.NODE_ELEMENT&&"br"==y.getName()&&(!CKEDITOR.env.needsBrFiller||y.getPrevious(l)||y.getNext(l))&&y.remove();this._.nextNode||(this._.nextNode=k||e.equals(p)||!p?null:this._getNextSourceNode(e,1,p));return e},_getNextSourceNode:function(a,b,c){function d(a){return!(a.equals(c)||a.equals(g))}var g= this.range.root;for(a=a.getNextSourceNode(b,null,d);!l(a);)a=a.getNextSourceNode(b,null,d);return a}};CKEDITOR.dom.range.prototype.createIterator=function(){return new a(this)}}(),CKEDITOR.command=function(a,f){this.uiItems=[];this.exec=function(b){if(this.state==CKEDITOR.TRISTATE_DISABLED||!this.checkAllowed())return!1;this.editorFocus&&a.focus();return!1===this.fire("exec")?!0:!1!==f.exec.call(this,a,b)};this.refresh=function(a,b){if(!this.readOnly&&a.readOnly)return!0;if(this.context&&!b.isContextFor(this.context)|| !this.checkAllowed(!0))return this.disable(),!0;this.startDisabled||this.enable();this.modes&&!this.modes[a.mode]&&this.disable();return!1===this.fire("refresh",{editor:a,path:b})?!0:f.refresh&&!1!==f.refresh.apply(this,arguments)};var b;this.checkAllowed=function(c){return c||"boolean"!=typeof b?b=a.activeFilter.checkFeature(this):b};CKEDITOR.tools.extend(this,f,{modes:{wysiwyg:1},editorFocus:1,contextSensitive:!!f.context,state:CKEDITOR.TRISTATE_DISABLED});CKEDITOR.event.call(this)},CKEDITOR.command.prototype= {enable:function(){this.state==CKEDITOR.TRISTATE_DISABLED&&this.checkAllowed()&&this.setState(this.preserveState&&"undefined"!=typeof this.previousState?this.previousState:CKEDITOR.TRISTATE_OFF)},disable:function(){this.setState(CKEDITOR.TRISTATE_DISABLED)},setState:function(a){if(this.state==a||a!=CKEDITOR.TRISTATE_DISABLED&&!this.checkAllowed())return!1;this.previousState=this.state;this.state=a;this.fire("state");return!0},toggleState:function(){this.state==CKEDITOR.TRISTATE_OFF?this.setState(CKEDITOR.TRISTATE_ON): this.state==CKEDITOR.TRISTATE_ON&&this.setState(CKEDITOR.TRISTATE_OFF)}},CKEDITOR.event.implementOn(CKEDITOR.command.prototype),CKEDITOR.ENTER_P=1,CKEDITOR.ENTER_BR=2,CKEDITOR.ENTER_DIV=3,CKEDITOR.config={customConfig:"config.js",autoUpdateElement:!0,language:"",defaultLanguage:"en",contentsLangDirection:"",enterMode:CKEDITOR.ENTER_P,forceEnterMode:!1,shiftEnterMode:CKEDITOR.ENTER_BR,docType:"\x3c!DOCTYPE html\x3e",bodyId:"",bodyClass:"",fullPage:!1,height:200,contentsCss:CKEDITOR.getUrl("contents.css"), extraPlugins:"",removePlugins:"",protectedSource:[],tabIndex:0,width:"",baseFloatZIndex:1E4,blockedKeystrokes:[CKEDITOR.CTRL+66,CKEDITOR.CTRL+73,CKEDITOR.CTRL+85]},function(){function a(a,b,c,e,d){var g,h;a=[];for(g in b){h=b[g];h="boolean"==typeof h?{}:"function"==typeof h?{match:h}:I(h);"$"!=g.charAt(0)&&(h.elements=g);c&&(h.featureName=c.toLowerCase());var f=h;f.elements=k(f.elements,/\s+/)||null;f.propertiesOnly=f.propertiesOnly||!0===f.elements;var m=/\s*,\s*/,l=void 0;for(l in L){f[l]=k(f[l], m)||null;var n=f,v=G[l],D=k(f[G[l]],m),x=f[l],A=[],B=!0,r=void 0;D?B=!1:D={};for(r in x)"!"==r.charAt(0)&&(r=r.slice(1),A.push(r),D[r]=!0,B=!1);for(;r=A.pop();)x[r]=x["!"+r],delete x["!"+r];n[v]=(B?!1:D)||null}f.match=f.match||null;e.push(h);a.push(h)}b=d.elements;d=d.generic;var F;c=0;for(e=a.length;c=--g&&(l&&CKEDITOR.document.getDocumentElement().removeStyle("cursor"),e(b))},q=function(b,c){a[b]=1;var e=f[b];delete f[b];for(var d=0;d=CKEDITOR.env.version||CKEDITOR.env.ie9Compat)?d.$.onreadystatechange=function(){if("loaded"==d.$.readyState||"complete"==d.$.readyState)d.$.onreadystatechange=null,q(b,!0)}:(d.$.onload=function(){setTimeout(function(){d.$.onload=null;d.$.onerror=null;q(b,!0)},0)},d.$.onerror=function(){d.$.onload=null;d.$.onerror=null;q(b,!1)}));d.appendTo(CKEDITOR.document.getHead())}}};l&&CKEDITOR.document.getDocumentElement().setStyle("cursor","wait");for(var u=0;u]+)>)|(?:!--([\S|\s]*?)--\x3e)|(?:([^\/\s>]+)((?:\s+[\w\-:.]+(?:\s*=\s*?(?:(?:"[^"]*")|(?:'[^']*')|[^\s"'\/>]+))?)*)[\S\s]*?(\/?)>))/g}},function(){var a=/([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g, f={checked:1,compact:1,declare:1,defer:1,disabled:1,ismap:1,multiple:1,nohref:1,noresize:1,noshade:1,nowrap:1,readonly:1,selected:1};CKEDITOR.htmlParser.prototype={onTagOpen:function(){},onTagClose:function(){},onText:function(){},onCDATA:function(){},onComment:function(){},parse:function(b){for(var c,d,l=0,k;c=this._.htmlPartsRegex.exec(b);){d=c.index;if(d>l)if(l=b.substring(l,d),k)k.push(l);else this.onText(l);l=this._.htmlPartsRegex.lastIndex;if(d=c[1])if(d=d.toLowerCase(),k&&CKEDITOR.dtd.$cdata[d]&& (this.onCDATA(k.join("")),k=null),!k){this.onTagClose(d);continue}if(k)k.push(c[0]);else if(d=c[3]){if(d=d.toLowerCase(),!/="/.test(d)){var g={},h,m=c[4];c=!!c[5];if(m)for(;h=a.exec(m);){var e=h[1].toLowerCase();h=h[2]||h[3]||h[4]||"";g[e]=!h&&f[e]?e:CKEDITOR.tools.htmlDecodeAttr(h)}this.onTagOpen(d,g,c);!k&&CKEDITOR.dtd.$cdata[d]&&(k=[])}}else if(d=c[2])this.onComment(d)}if(b.length>l)this.onText(b.substring(l,b.length))}}}(),CKEDITOR.htmlParser.basicWriter=CKEDITOR.tools.createClass({$:function(){this._= {output:[]}},proto:{openTag:function(a){this._.output.push("\x3c",a)},openTagClose:function(a,f){f?this._.output.push(" /\x3e"):this._.output.push("\x3e")},attribute:function(a,f){"string"==typeof f&&(f=CKEDITOR.tools.htmlEncodeAttr(f));this._.output.push(" ",a,'\x3d"',f,'"')},closeTag:function(a){this._.output.push("\x3c/",a,"\x3e")},text:function(a){this._.output.push(a)},comment:function(a){this._.output.push("\x3c!--",a,"--\x3e")},write:function(a){this._.output.push(a)},reset:function(){this._.output= [];this._.indent=!1},getHtml:function(a){var f=this._.output.join("");a&&this.reset();return f}}}),"use strict",function(){CKEDITOR.htmlParser.node=function(){};CKEDITOR.htmlParser.node.prototype={remove:function(){var a=this.parent.children,f=CKEDITOR.tools.indexOf(a,this),b=this.previous,c=this.next;b&&(b.next=c);c&&(c.previous=b);a.splice(f,1);this.parent=null},replaceWith:function(a){var f=this.parent.children,b=CKEDITOR.tools.indexOf(f,this),c=a.previous=this.previous,d=a.next=this.next;c&&(c.next= a);d&&(d.previous=a);f[b]=a;a.parent=this.parent;this.parent=null},insertAfter:function(a){var f=a.parent.children,b=CKEDITOR.tools.indexOf(f,a),c=a.next;f.splice(b+1,0,this);this.next=a.next;this.previous=a;a.next=this;c&&(c.previous=this);this.parent=a.parent},insertBefore:function(a){var f=a.parent.children,b=CKEDITOR.tools.indexOf(f,a);f.splice(b,0,this);this.next=a;(this.previous=a.previous)&&(a.previous.next=this);a.previous=this;this.parent=a.parent},getAscendant:function(a){var f="function"== typeof a?a:"string"==typeof a?function(b){return b.name==a}:function(b){return b.name in a},b=this.parent;for(;b&&b.type==CKEDITOR.NODE_ELEMENT;){if(f(b))return b;b=b.parent}return null},wrapWith:function(a){this.replaceWith(a);a.add(this);return a},getIndex:function(){return CKEDITOR.tools.indexOf(this.parent.children,this)},getFilterContext:function(a){return a||{}}}}(),"use strict",CKEDITOR.htmlParser.comment=function(a){this.value=a;this._={isBlockLike:!1}},CKEDITOR.htmlParser.comment.prototype= CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_COMMENT,filter:function(a,f){var b=this.value;if(!(b=a.onComment(f,b,this)))return this.remove(),!1;if("string"!=typeof b)return this.replaceWith(b),!1;this.value=b;return!0},writeHtml:function(a,f){f&&this.filter(f);a.comment(this.value)}}),"use strict",function(){CKEDITOR.htmlParser.text=function(a){this.value=a;this._={isBlockLike:!1}};CKEDITOR.htmlParser.text.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_TEXT, filter:function(a,f){if(!(this.value=a.onText(f,this.value,this)))return this.remove(),!1},writeHtml:function(a,f){f&&this.filter(f);a.text(this.value)}})}(),"use strict",function(){CKEDITOR.htmlParser.cdata=function(a){this.value=a};CKEDITOR.htmlParser.cdata.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_TEXT,filter:function(){},writeHtml:function(a){a.write(this.value)}})}(),"use strict",CKEDITOR.htmlParser.fragment=function(){this.children=[];this.parent=null; this._={isBlockLike:!0,hasInlineStarted:!1}},function(){function a(a){return a.attributes["data-cke-survive"]?!1:"a"==a.name&&a.attributes.href||CKEDITOR.dtd.$removeEmpty[a.name]}var f=CKEDITOR.tools.extend({table:1,ul:1,ol:1,dl:1},CKEDITOR.dtd.table,CKEDITOR.dtd.ul,CKEDITOR.dtd.ol,CKEDITOR.dtd.dl),b={ol:1,ul:1},c=CKEDITOR.tools.extend({},{html:1},CKEDITOR.dtd.html,CKEDITOR.dtd.body,CKEDITOR.dtd.head,{style:1,script:1}),d={ul:"li",ol:"li",dl:"dd",table:"tbody",tbody:"tr",thead:"tr",tfoot:"tr",tr:"td"}; CKEDITOR.htmlParser.fragment.fromHtml=function(l,k,g){function h(a){var b;if(0k;k++)if(f=d[k]){f= f.exec(a,c,this);if(!1===f)return null;if(f&&f!=c)return this.onNode(a,f);if(c.parent&&!c.name)break}return c},onNode:function(a,c){var d=c.type;return d==CKEDITOR.NODE_ELEMENT?this.onElement(a,c):d==CKEDITOR.NODE_TEXT?new CKEDITOR.htmlParser.text(this.onText(a,c.value)):d==CKEDITOR.NODE_COMMENT?new CKEDITOR.htmlParser.comment(this.onComment(a,c.value)):null},onAttribute:function(a,c,d,f){return(d=this.attributesRules[d])?d.exec(a,f,c,this):f}}});CKEDITOR.htmlParser.filterRulesGroup=a;a.prototype= {add:function(a,c,d){this.rules.splice(this.findIndex(c),0,{value:a,priority:c,options:d})},addMany:function(a,c,d){for(var f=[this.findIndex(c),0],k=0,g=a.length;k/g,"\x26gt;")+"\x3c/textarea\x3e");return"\x3ccke:encoded\x3e"+encodeURIComponent(a)+"\x3c/cke:encoded\x3e"})}function n(a){return a.replace(D,function(a,b){return decodeURIComponent(b)})}function q(a){return a.replace(/\x3c!--(?!{cke_protected})[\s\S]+?--\x3e/g, function(a){return"\x3c!--"+z+"{C}"+encodeURIComponent(a).replace(/--/g,"%2D%2D")+"--\x3e"})}function y(a){return CKEDITOR.tools.array.reduce(a.split(""),function(a,b){var c=b.toLowerCase(),e=b.toUpperCase(),d=u(c);c!==e&&(d+="|"+u(e));return a+("("+d+")")},"")}function u(a){var b;b=a.charCodeAt(0);var c=b.toString(16);b={htmlCode:"\x26#"+b+";?",hex:"\x26#x0*"+c+";?",entity:{"\x3c":"\x26lt;","\x3e":"\x26gt;",":":"\x26colon;"}[a]};for(var e in b)b[e]&&(a+="|"+b[e]);return a}function p(a){return a.replace(/\x3c!--\{cke_protected\}\{C\}([\s\S]+?)--\x3e/g, function(a,b){return decodeURIComponent(b)})}function v(a,b){var c=b._.dataStore;return a.replace(/\x3c!--\{cke_protected\}([\s\S]+?)--\x3e/g,function(a,b){return decodeURIComponent(b)}).replace(/\{cke_protected_(\d+)\}/g,function(a,b){return c&&c[b]||""})}function w(a,b){var c=[],e=b.config.protectedSource,d=b._.dataStore||(b._.dataStore={id:1}),g=/<\!--\{cke_temp(comment)?\}(\d*?)--\x3e/g,e=[/|$)/gi,//gi,//gi].concat(e);a=a.replace(/\x3c!--[\s\S]*?--\x3e/g, function(a){return"\x3c!--{cke_tempcomment}"+(c.push(a)-1)+"--\x3e"});for(var h=0;h]+\s*=\s*(?:[^'"\s>]+|'[^']*'|"[^"]*"))|[^\s=\/>]+))+\s*\/?>/g,function(a){return a.replace(/\x3c!--\{cke_protected\}([^>]*)--\x3e/g, function(a,b){d[d.id]=decodeURIComponent(b);return"{cke_protected_"+d.id++ +"}"})});return a=a.replace(/<(title|iframe|textarea)([^>]*)>([\s\S]*?)<\/\1>/g,function(a,c,e,d){return"\x3c"+c+e+"\x3e"+v(p(d),b)+"\x3c/"+c+"\x3e"})}CKEDITOR.htmlDataProcessor=function(b){var c,d,g=this;this.editor=b;this.dataFilter=c=new CKEDITOR.htmlParser.filter;this.htmlFilter=d=new CKEDITOR.htmlParser.filter;this.writer=new CKEDITOR.htmlParser.basicWriter;c.addRules(B);c.addRules(C,{applyToAll:!0});c.addRules(a(b,"data"), {applyToAll:!0});d.addRules(H);d.addRules(F,{applyToAll:!0});d.addRules(a(b,"html"),{applyToAll:!0});b.on("toHtml",function(a){a=a.data;var c=a.dataValue,d,c=c.replace(N,""),c=w(c,b),c=e(c,G),c=m(c),c=e(c,L),c=c.replace(Q,"$1cke:$2"),c=c.replace(K,"\x3ccke:$1$2\x3e\x3c/cke:$1\x3e"),c=c.replace(/(]*>)(\r\n|\n)/g,"$1$2$2"),c=c.replace(/([^a-z0-9<\-])(on\w{3,})(?!>)/gi,"$1data-cke-"+CKEDITOR.rnd+"-$2");d=a.context||b.editable().getName();var g;CKEDITOR.env.ie&&9>CKEDITOR.env.version&&"pre"== d&&(d="div",c="\x3cpre\x3e"+c+"\x3c/pre\x3e",g=1);d=b.document.createElement(d);d.setHtml("a"+c);c=d.getHtml().substr(1);c=c.replace(new RegExp("data-cke-"+CKEDITOR.rnd+"-","ig"),"");g&&(c=c.replace(/^
|<\/pre>$/gi,""));c=c.replace(O,"$1$2");c=n(c);c=p(c);d=!1===a.fixForBody?!1:f(a.enterMode,b.config.autoParagraph);c=CKEDITOR.htmlParser.fragment.fromHtml(c,a.context,d);d&&(g=c,!g.children.length&&CKEDITOR.dtd[g.name][d]&&(d=new CKEDITOR.htmlParser.element(d),g.add(d)));a.dataValue=c},null,null,
5);b.on("toHtml",function(a){a.data.filter.applyTo(a.data.dataValue,!0,a.data.dontFilter,a.data.enterMode)&&b.fire("dataFiltered")},null,null,6);b.on("toHtml",function(a){a.data.dataValue.filterChildren(g.dataFilter,!0)},null,null,10);b.on("toHtml",function(a){a=a.data;var b=a.dataValue,c=new CKEDITOR.htmlParser.basicWriter;b.writeChildrenHtml(c);b=c.getHtml(!0);a.dataValue=q(b)},null,null,15);b.on("toDataFormat",function(a){var c=a.data.dataValue;a.data.enterMode!=CKEDITOR.ENTER_BR&&(c=c.replace(/^
/i, ""));a.data.dataValue=CKEDITOR.htmlParser.fragment.fromHtml(c,a.data.context,f(a.data.enterMode,b.config.autoParagraph))},null,null,5);b.on("toDataFormat",function(a){a.data.dataValue.filterChildren(g.htmlFilter,!0)},null,null,10);b.on("toDataFormat",function(a){a.data.filter.applyTo(a.data.dataValue,!1,!0)},null,null,11);b.on("toDataFormat",function(a){var c=a.data.dataValue,e=g.writer;e.reset();c.writeChildrenHtml(e);c=e.getHtml(!0);c=p(c);c=v(c,b);a.data.dataValue=c},null,null,15)};CKEDITOR.htmlDataProcessor.prototype= {toHtml:function(a,b,c,e){var d=this.editor,g,h,f,m;b&&"object"==typeof b?(g=b.context,c=b.fixForBody,e=b.dontFilter,h=b.filter,f=b.enterMode,m=b.protectedWhitespaces):g=b;g||null===g||(g=d.editable().getName());return d.fire("toHtml",{dataValue:a,context:g,fixForBody:c,dontFilter:e,filter:h||d.filter,enterMode:f||d.enterMode,protectedWhitespaces:m}).dataValue},toDataFormat:function(a,b){var c,e,d;b&&(c=b.context,e=b.filter,d=b.enterMode);c||null===c||(c=this.editor.editable().getName());return this.editor.fire("toDataFormat", {dataValue:a,filter:e||this.editor.filter,context:c,enterMode:d||this.editor.enterMode}).dataValue}};var r=/(?: |\xa0)$/,z="{cke_protected}",t=CKEDITOR.dtd,x="caption colgroup col thead tfoot tbody".split(" "),A=CKEDITOR.tools.extend({},t.$blockLimit,t.$block),B={elements:{input:g,textarea:g}},C={attributeNames:[[/^on/,"data-cke-pa-on"],[/^srcdoc/,"data-cke-pa-srcdoc"],[/^data-cke-expando$/,""]],elements:{iframe:function(a){if(a.attributes&&a.attributes.src){var b=a.attributes.src.toLowerCase().replace(/[^a-z]/gi, "");if(0===b.indexOf("javascript")||0===b.indexOf("data"))a.attributes["data-cke-pa-src"]=a.attributes.src,delete a.attributes.src}}}},H={elements:{embed:function(a){var b=a.parent;if(b&&"object"==b.name){var c=b.attributes.width,b=b.attributes.height;c&&(a.attributes.width=c);b&&(a.attributes.height=b)}},a:function(a){var b=a.attributes;if(!(a.children.length||b.name||b.id||a.attributes["data-cke-saved-name"]))return!1}}},F={elementNames:[[/^cke:/,""],[/^\?xml:namespace$/,""]],attributeNames:[[/^data-cke-(saved|pa)-/, ""],[/^data-cke-.*/,""],["hidefocus",""]],elements:{$:function(a){var b=a.attributes;if(b){if(b["data-cke-temp"])return!1;for(var c=["name","href","src"],e,d=0;de? 1:-1})},param:function(a){a.children=[];a.isEmpty=!0;return a},span:function(a){"Apple-style-span"==a.attributes["class"]&&delete a.name},html:function(a){delete a.attributes.contenteditable;delete a.attributes["class"]},body:function(a){delete a.attributes.spellcheck;delete a.attributes.contenteditable},style:function(a){var b=a.children[0];b&&b.value&&(b.value=CKEDITOR.tools.trim(b.value));a.attributes.type||(a.attributes.type="text/css")},title:function(a){var b=a.children[0];!b&&k(a,b=new CKEDITOR.htmlParser.text); b.value=a.attributes["data-cke-title"]||""},input:h,textarea:h},attributes:{"class":function(a){return CKEDITOR.tools.ltrim(a.replace(/(?:^|\s+)cke_[^\s]*/g,""))||!1}}};CKEDITOR.env.ie&&(F.attributes.style=function(a){return a.replace(/(^|;)([^\:]+)/g,function(a){return a.toLowerCase()})});var I=/<(a|area|img|input|source)\b([^>]*)>/gi,J=/([\w-:]+)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi,E=/^(href|src|name)$/i,L=/(?:])[^>]*>[\s\S]*?<\/style>)|(?:<(:?link|meta|base)[^>]*>)/gi, G=/(])[^>]*>)([\s\S]*?)(?:<\/textarea>)/gi,D=/([^<]*)<\/cke:encoded>/gi,N=new RegExp("("+y("\x3ccke:encoded\x3e")+"(.*?)"+y("\x3c/cke:encoded\x3e")+")|("+y("\x3c")+y("/")+"?"+y("cke:encoded\x3e")+")","gi"),Q=/(<\/?)((?:object|embed|param|html|body|head|title)([\s][^>]*)?>)/gi,O=/(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi,K=/]*?)\/?>(?!\s*<\/cke:\1)/gi}(),"use strict",CKEDITOR.htmlParser.element=function(a,f){this.name=a;this.attributes=f||{};this.children= [];var b=a||"",c=b.match(/^cke:(.*)/);c&&(b=c[1]);b=!!(CKEDITOR.dtd.$nonBodyContent[b]||CKEDITOR.dtd.$block[b]||CKEDITOR.dtd.$listItem[b]||CKEDITOR.dtd.$tableContent[b]||CKEDITOR.dtd.$nonEditable[b]||"br"==b);this.isEmpty=!!CKEDITOR.dtd.$empty[a];this.isUnknown=!CKEDITOR.dtd[a];this._={isBlockLike:b,hasInlineStarted:this.isEmpty||!b}},CKEDITOR.htmlParser.cssStyle=function(a){var f={};((a instanceof CKEDITOR.htmlParser.element?a.attributes.style:a)||"").replace(/"/g,'"').replace(/\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function(a,c,d){"font-family"==c&&(d=d.replace(/["']/g,""));f[c.toLowerCase()]=d});return{rules:f,populate:function(a){var c=this.toString();c&&(a instanceof CKEDITOR.dom.element?a.setAttribute("style",c):a instanceof CKEDITOR.htmlParser.element?a.attributes.style=c:a.style=c)},toString:function(){var a=[],c;for(c in f)f[c]&&a.push(c,":",f[c],";");return a.join("")}}},function(){function a(a){return function(b){return b.type==CKEDITOR.NODE_ELEMENT&&("string"==typeof a?b.name==a:b.name in a)}}var f= function(a,b){a=a[0];b=b[0];return ab?1:0},b=CKEDITOR.htmlParser.fragment.prototype;CKEDITOR.htmlParser.element.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_ELEMENT,add:b.add,clone:function(){return new CKEDITOR.htmlParser.element(this.name,this.attributes)},filter:function(a,b){var f=this,k,g;b=f.getFilterContext(b);if(!f.parent)a.onRoot(b,f);for(;;){k=f.name;if(!(g=a.onElementName(b,k)))return this.remove(),!1;f.name=g;if(!(f=a.onElement(b,f)))return this.remove(), !1;if(f!==this)return this.replaceWith(f),!1;if(f.name==k)break;if(f.type!=CKEDITOR.NODE_ELEMENT)return this.replaceWith(f),!1;if(!f.name)return this.replaceWithChildren(),!1}k=f.attributes;var h,m;for(h in k){for(g=k[h];;)if(m=a.onAttributeName(b,h))if(m!=h)delete k[h],h=m;else break;else{delete k[h];break}m&&(!1===(g=a.onAttribute(b,f,m,g))?delete k[m]:k[m]=g)}f.isEmpty||this.filterChildren(a,!1,b);return!0},filterChildren:b.filterChildren,writeHtml:function(a,b){b&&this.filter(b);var l=this.name, k=[],g=this.attributes,h,m;a.openTag(l,g);for(h in g)k.push([h,g[h]]);a.sortAttributes&&k.sort(f);h=0;for(m=k.length;hCKEDITOR.env.version||CKEDITOR.env.quirks))this.hasFocus&&(this.focus(),b());else if(this.hasFocus)this.focus(), a();else this.once("focus",function(){a()},null,null,-999)},getHtmlFromRange:function(a){if(a.collapsed)return new CKEDITOR.dom.documentFragment(a.document);a={doc:this.getDocument(),range:a.clone()};z.eol.detect(a,this);z.bogus.exclude(a);z.cell.shrink(a);a.fragment=a.range.cloneContents();z.tree.rebuild(a,this);z.eol.fix(a,this);return new CKEDITOR.dom.documentFragment(a.fragment.$)},extractHtmlFromRange:function(a,b){var c=t,e={range:a,doc:a.document},d=this.getHtmlFromRange(a);if(a.collapsed)return a.optimize(), d;a.enlarge(CKEDITOR.ENLARGE_INLINE,1);c.table.detectPurge(e);e.bookmark=a.createBookmark();delete e.range;var g=this.editor.createRange();g.moveToPosition(e.bookmark.startNode,CKEDITOR.POSITION_BEFORE_START);e.targetBookmark=g.createBookmark();c.list.detectMerge(e,this);c.table.detectRanges(e,this);c.block.detectMerge(e,this);e.tableContentsRanges?(c.table.deleteRanges(e),a.moveToBookmark(e.bookmark),e.range=a):(a.moveToBookmark(e.bookmark),e.range=a,a.extractContents(c.detectExtractMerge(e)));a.moveToBookmark(e.targetBookmark); a.optimize();c.fixUneditableRangePosition(a);c.list.merge(e,this);c.table.purge(e,this);c.block.merge(e,this);if(b){c=a.startPath();if(e=a.checkStartOfBlock()&&a.checkEndOfBlock()&&c.block&&!a.root.equals(c.block)){a:{var e=c.block.getElementsByTag("span"),g=0,f;if(e)for(;f=e.getItem(g++);)if(!q(f)){e=!0;break a}e=!1}e=!e}e&&(a.moveToPosition(c.block,CKEDITOR.POSITION_BEFORE_START),c.block.remove())}else c.autoParagraph(this.editor,a),y(a.startContainer)&&a.startContainer.appendBogus();a.startContainer.mergeSiblings(); return d},setup:function(){var a=this.editor;this.attachListener(a,"beforeGetData",function(){var b=this.getData();this.is("textarea")||!1!==a.config.ignoreEmptyParagraph&&(b=b.replace(p,function(a,b){return b}));a.setData(b,null,1)},this);this.attachListener(a,"getSnapshot",function(a){a.data=this.getData(1)},this);this.attachListener(a,"afterSetData",function(){this.setData(a.getData(1))},this);this.attachListener(a,"loadSnapshot",function(a){this.setData(a.data,1)},this);this.attachListener(a, "beforeFocus",function(){var b=a.getSelection();(b=b&&b.getNative())&&"Control"==b.type||this.focus()},this);this.attachListener(a,"insertHtml",function(a){this.insertHtml(a.data.dataValue,a.data.mode,a.data.range)},this);this.attachListener(a,"insertElement",function(a){this.insertElement(a.data)},this);this.attachListener(a,"insertText",function(a){this.insertText(a.data)},this);this.setReadOnly(a.readOnly);this.attachClass("cke_editable");a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?this.attachClass("cke_editable_inline"): a.elementMode!=CKEDITOR.ELEMENT_MODE_REPLACE&&a.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO||this.attachClass("cke_editable_themed");this.attachClass("cke_contents_"+a.config.contentsLangDirection);a.keystrokeHandler.blockedKeystrokes[8]=+a.readOnly;a.keystrokeHandler.attach(this);this.on("blur",function(){this.hasFocus=!1},null,null,-1);this.on("focus",function(){this.hasFocus=!0},null,null,-1);if(CKEDITOR.env.webkit)this.on("scroll",function(){a._.previousScrollTop=a.editable().$.scrollTop},null, null,-1);if(CKEDITOR.env.edge&&14CKEDITOR.env.version?m.$.styleSheet.cssText=h:m.setText(h)):(h=g.appendStyleText(h),h=new CKEDITOR.dom.element(h.ownerNode||h.owningElement),f.setCustomData("stylesheet", h),h.data("cke-temp",1))}f=g.getCustomData("stylesheet_ref")||0;g.setCustomData("stylesheet_ref",f+1);this.setCustomData("cke_includeReadonly",!a.config.disableReadonlyStyling);this.attachListener(this,"click",function(a){a=a.data;var b=(new CKEDITOR.dom.elementPath(a.getTarget(),this)).contains("a");b&&2!=a.$.button&&b.isReadOnly()&&a.preventDefault()});var k={8:1,46:1};this.attachListener(a,"key",function(b){if(a.readOnly)return!0;var c=b.data.domEvent.getKey(),e;b=a.getSelection();if(0!==b.getRanges().length){if(c in k){var d,g=b.getRanges()[0],f=g.startPath(),h,m,q,c=8==c;CKEDITOR.env.ie&&11>CKEDITOR.env.version&&(d=b.getSelectedElement())||(d=l(b))?(a.fire("saveSnapshot"),g.moveToPosition(d,CKEDITOR.POSITION_BEFORE_START),d.remove(),g.select(),a.fire("saveSnapshot"),e=1):g.collapsed&&((h=f.block)&&(q=h[c?"getPrevious":"getNext"](n))&&q.type==CKEDITOR.NODE_ELEMENT&&q.is("table")&&g[c?"checkStartOfBlock":"checkEndOfBlock"]()?(a.fire("saveSnapshot"),g[c?"checkEndOfBlock":"checkStartOfBlock"]()&&h.remove(),g["moveToElementEdit"+ (c?"End":"Start")](q),g.select(),a.fire("saveSnapshot"),e=1):f.blockLimit&&f.blockLimit.is("td")&&(m=f.blockLimit.getAscendant("table"))&&g.checkBoundaryOfElement(m,c?CKEDITOR.START:CKEDITOR.END)&&(q=m[c?"getPrevious":"getNext"](n))?(a.fire("saveSnapshot"),g["moveToElementEdit"+(c?"End":"Start")](q),g.checkStartOfBlock()&&g.checkEndOfBlock()?q.remove():g.select(),a.fire("saveSnapshot"),e=1):(m=f.contains(["td","th","caption"]))&&g.checkBoundaryOfElement(m,c?CKEDITOR.START:CKEDITOR.END)&&(e=1))}return!e}}); a.blockless&&CKEDITOR.env.ie&&CKEDITOR.env.needsBrFiller&&this.attachListener(this,"keyup",function(b){b.data.getKeystroke()in k&&!this.getFirst(c)&&(this.appendBogus(),b=a.createRange(),b.moveToPosition(this,CKEDITOR.POSITION_AFTER_START),b.select())});this.attachListener(this,"dblclick",function(b){if(a.readOnly)return!1;b={element:b.data.getTarget()};a.fire("doubleclick",b)});CKEDITOR.env.ie&&this.attachListener(this,"click",b);CKEDITOR.env.ie&&!CKEDITOR.env.edge||this.attachListener(this,"mousedown", function(b){var c=b.data.getTarget();c.is("img","hr","input","textarea","select")&&!c.isReadOnly()&&(a.getSelection().selectElement(c),c.is("input","textarea","select")&&b.data.preventDefault())});CKEDITOR.env.edge&&this.attachListener(this,"mouseup",function(b){(b=b.data.getTarget())&&b.is("img")&&!b.isReadOnly()&&a.getSelection().selectElement(b)});CKEDITOR.env.gecko&&this.attachListener(this,"mouseup",function(b){if(2==b.data.$.button&&(b=b.data.getTarget(),!b.getAscendant("table")&&!b.getOuterHtml().replace(p, ""))){var c=a.createRange();c.moveToElementEditStart(b);c.select(!0)}});CKEDITOR.env.webkit&&(this.attachListener(this,"click",function(a){a.data.getTarget().is("input","select")&&a.data.preventDefault()}),this.attachListener(this,"mouseup",function(a){a.data.getTarget().is("input","textarea")&&a.data.preventDefault()}));CKEDITOR.env.webkit&&this.attachListener(a,"key",function(b){if(a.readOnly)return!0;var c=b.data.domEvent.getKey();if(c in k&&(b=a.getSelection(),0!==b.getRanges().length)){var c= 8==c,d=b.getRanges()[0];b=d.startPath();if(d.collapsed)a:{var g=b.block;if(g&&d[c?"checkStartOfBlock":"checkEndOfBlock"]()&&d.moveToClosestEditablePosition(g,!c)&&d.collapsed){if(d.startContainer.type==CKEDITOR.NODE_ELEMENT){var f=d.startContainer.getChild(d.startOffset-(c?1:0));if(f&&f.type==CKEDITOR.NODE_ELEMENT&&f.is("hr")){a.fire("saveSnapshot");f.remove();b=!0;break a}}d=d.startPath().block;if(!d||d&&d.contains(g))b=void 0;else{a.fire("saveSnapshot");var h;(h=(c?d:g).getBogus())&&h.remove(); h=a.getSelection();f=h.createBookmarks();(c?g:d).moveChildren(c?d:g,!1);b.lastElement.mergeSiblings();e(g,d,!c);h.selectBookmarks(f);b=!0}}else b=!1}else c=d,h=b.block,d=c.endPath().block,h&&d&&!h.equals(d)?(a.fire("saveSnapshot"),(g=h.getBogus())&&g.remove(),c.enlarge(CKEDITOR.ENLARGE_INLINE),c.deleteContents(),d.getParent()&&(d.moveChildren(h,!1),b.lastElement.mergeSiblings(),e(h,d,!0)),c=a.getSelection().getRanges()[0],c.collapse(1),c.optimize(),""===c.startContainer.getHtml()&&c.startContainer.appendBogus(), c.select(),b=!0):b=!1;if(!b)return;a.getSelection().scrollIntoView();a.fire("saveSnapshot");return!1}},this,null,100)}}},_:{detach:function(){this.editor.setData(this.editor.getData(),0,1);this.clearListeners();this.restoreAttrs();var a;if(a=this.removeCustomData("classes"))for(;a.length;)this.removeClass(a.pop());if(!this.is("textarea")){a=this.getDocument();var b=a.getHead();if(b.getCustomData("stylesheet")){var c=a.getCustomData("stylesheet_ref");--c?a.setCustomData("stylesheet_ref",c):(a.removeCustomData("stylesheet_ref"), b.removeCustomData("stylesheet").remove())}}this.editor.fire("contentDomUnload");delete this.editor}}});CKEDITOR.editor.prototype.editable=function(a){var b=this._.editable;if(b&&a)return 0;arguments.length&&(b=this._.editable=a?a instanceof CKEDITOR.editable?a:new CKEDITOR.editable(this,a):(b&&b.detach(),null));return b};CKEDITOR.on("instanceLoaded",function(b){var c=b.editor;c.on("insertElement",function(a){a=a.data;a.type==CKEDITOR.NODE_ELEMENT&&(a.is("input")||a.is("textarea"))&&("false"!=a.getAttribute("contentEditable")&& a.data("cke-editable",a.hasAttribute("contenteditable")?"true":"1"),a.setAttribute("contentEditable",!1))});c.on("selectionChange",function(b){if(!c.readOnly){var e=c.getSelection();e&&!e.isLocked&&(e=c.checkDirty(),c.fire("lockSnapshot"),a(b),c.fire("unlockSnapshot"),!e&&c.resetDirty())}})});CKEDITOR.on("instanceCreated",function(a){var b=a.editor;b.on("mode",function(){var a=b.editable();if(a&&a.isInline()){var c=b.title;a.changeAttr("role","textbox");a.changeAttr("aria-multiline","true");a.changeAttr("aria-label", c);c&&a.changeAttr("title",c);var e=b.fire("ariaEditorHelpLabel",{}).label;if(e&&(c=this.ui.space(this.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?"top":"contents"))){var d=CKEDITOR.tools.getNextId(),e=CKEDITOR.dom.element.createFromHtml('\x3cspan id\x3d"'+d+'" class\x3d"cke_voice_label"\x3e'+e+"\x3c/span\x3e");c.append(e);a.changeAttr("aria-describedby",d)}}})});CKEDITOR.addCss(".cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}");n=CKEDITOR.dom.walker.whitespaces(!0); q=CKEDITOR.dom.walker.bookmark(!1,!0);y=CKEDITOR.dom.walker.empty();u=CKEDITOR.dom.walker.bogus();p=/(^|]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:]*>| |\u00A0| )?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;v=function(){function a(b){return b.type==CKEDITOR.NODE_ELEMENT}function b(c,e){var d,g,f,h,m=[],k=e.range.startContainer;d=e.range.startPath();for(var k=n[k.getName()],l=0,q=c.getChildren(),G=q.count(),v=-1,r=-1,F=0,t=d.contains(n.$list);lCKEDITOR.env.version&&e.getChildCount()&&e.getFirst().remove())}return function(e){var d=e.startContainer,g=d.getAscendant("table",1),f=!1;c(g.getElementsByTag("td"));c(g.getElementsByTag("th"));g=e.clone();g.setStart(d,0);g= a(g).lastBackward();g||(g=e.clone(),g.setEndAt(d,CKEDITOR.POSITION_BEFORE_END),g=a(g).lastForward(),f=!0);g||(g=d);g.is("table")?(e.setStartAt(g,CKEDITOR.POSITION_BEFORE_START),e.collapse(!0),g.remove()):(g.is({tbody:1,thead:1,tfoot:1})&&(g=b(g,"tr",f)),g.is("tr")&&(g=b(g,g.getParent().is("thead")?"th":"td",f)),(d=g.getBogus())&&d.remove(),e.moveToPosition(g,f?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END))}}();r=function(){function a(b){b=new CKEDITOR.dom.walker(b);b.guard=function(a, b){if(b)return!1;if(a.type==CKEDITOR.NODE_ELEMENT)return a.is(CKEDITOR.dtd.$list)||a.is(CKEDITOR.dtd.$listItem)};b.evaluator=function(a){return a.type==CKEDITOR.NODE_ELEMENT&&a.is(CKEDITOR.dtd.$listItem)};return b}return function(b){var c=b.startContainer,e=!1,d;d=b.clone();d.setStart(c,0);d=a(d).lastBackward();d||(d=b.clone(),d.setEndAt(c,CKEDITOR.POSITION_BEFORE_END),d=a(d).lastForward(),e=!0);d||(d=c);d.is(CKEDITOR.dtd.$list)?(b.setStartAt(d,CKEDITOR.POSITION_BEFORE_START),b.collapse(!0),d.remove()): ((c=d.getBogus())&&c.remove(),b.moveToPosition(d,e?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END),b.select())}}();z={eol:{detect:function(a,b){var c=a.range,e=c.clone(),d=c.clone(),g=new CKEDITOR.dom.elementPath(c.startContainer,b),f=new CKEDITOR.dom.elementPath(c.endContainer,b);e.collapse(1);d.collapse();g.block&&e.checkBoundaryOfElement(g.block,CKEDITOR.END)&&(c.setStartAfter(g.block),a.prependEolBr=1);f.block&&d.checkBoundaryOfElement(f.block,CKEDITOR.START)&&(c.setEndBefore(f.block), a.appendEolBr=1)},fix:function(a,b){var c=b.getDocument(),e;a.appendEolBr&&(e=this.createEolBr(c),a.fragment.append(e));!a.prependEolBr||e&&!e.getPrevious()||a.fragment.append(this.createEolBr(c),1)},createEolBr:function(a){return a.createElement("br",{attributes:{"data-cke-eol":1}})}},bogus:{exclude:function(a){var b=a.range.getBoundaryNodes(),c=b.startNode,b=b.endNode;!b||!u(b)||c&&c.equals(b)||a.range.setEndBefore(b)}},tree:{rebuild:function(a,b){var c=a.range,e=c.getCommonAncestor(),d=new CKEDITOR.dom.elementPath(e, b),g=new CKEDITOR.dom.elementPath(c.startContainer,b),c=new CKEDITOR.dom.elementPath(c.endContainer,b),f;e.type==CKEDITOR.NODE_TEXT&&(e=e.getParent());if(d.blockLimit.is({tr:1,table:1})){var h=d.contains("table").getParent();f=function(a){return!a.equals(h)}}else if(d.block&&d.block.is(CKEDITOR.dtd.$listItem)&&(g=g.contains(CKEDITOR.dtd.$list),c=c.contains(CKEDITOR.dtd.$list),!g.equals(c))){var m=d.contains(CKEDITOR.dtd.$list).getParent();f=function(a){return!a.equals(m)}}f||(f=function(a){return!a.equals(d.block)&& !a.equals(d.blockLimit)});this.rebuildFragment(a,b,e,f)},rebuildFragment:function(a,b,c,e){for(var d;c&&!c.equals(b)&&e(c);)d=c.clone(0,1),a.fragment.appendTo(d),a.fragment=d,c=c.getParent()}},cell:{shrink:function(a){a=a.range;var b=a.startContainer,c=a.endContainer,e=a.startOffset,d=a.endOffset;b.type==CKEDITOR.NODE_ELEMENT&&b.equals(c)&&b.is("tr")&&++e==d&&a.shrink(CKEDITOR.SHRINK_TEXT)}}};t=function(){function a(b,c){var e=b.getParent();if(e.is(CKEDITOR.dtd.$inline))b[c?"insertBefore":"insertAfter"](e)} function b(c,e,d){a(e);a(d,1);for(var g;g=d.getNext();)g.insertAfter(e),e=g;y(c)&&c.remove()}function c(a,b){var e=new CKEDITOR.dom.range(a);e.setStartAfter(b.startNode);e.setEndBefore(b.endNode);return e}return{list:{detectMerge:function(a,b){var e=c(b,a.bookmark),d=e.startPath(),g=e.endPath(),f=d.contains(CKEDITOR.dtd.$list),h=g.contains(CKEDITOR.dtd.$list);a.mergeList=f&&h&&f.getParent().equals(h.getParent())&&!f.equals(h);a.mergeListItems=d.block&&g.block&&d.block.is(CKEDITOR.dtd.$listItem)&& g.block.is(CKEDITOR.dtd.$listItem);if(a.mergeList||a.mergeListItems)e=e.clone(),e.setStartBefore(a.bookmark.startNode),e.setEndAfter(a.bookmark.endNode),a.mergeListBookmark=e.createBookmark()},merge:function(a,c){if(a.mergeListBookmark){var e=a.mergeListBookmark.startNode,d=a.mergeListBookmark.endNode,g=new CKEDITOR.dom.elementPath(e,c),f=new CKEDITOR.dom.elementPath(d,c);if(a.mergeList){var h=g.contains(CKEDITOR.dtd.$list),m=f.contains(CKEDITOR.dtd.$list);h.equals(m)||(m.moveChildren(h),m.remove())}a.mergeListItems&& (g=g.contains(CKEDITOR.dtd.$listItem),f=f.contains(CKEDITOR.dtd.$listItem),g.equals(f)||b(f,e,d));e.remove();d.remove()}}},block:{detectMerge:function(a,b){if(!a.tableContentsRanges&&!a.mergeListBookmark){var c=new CKEDITOR.dom.range(b);c.setStartBefore(a.bookmark.startNode);c.setEndAfter(a.bookmark.endNode);a.mergeBlockBookmark=c.createBookmark()}},merge:function(a,c){if(a.mergeBlockBookmark&&!a.purgeTableBookmark){var e=a.mergeBlockBookmark.startNode,d=a.mergeBlockBookmark.endNode,g=new CKEDITOR.dom.elementPath(e, c),f=new CKEDITOR.dom.elementPath(d,c),g=g.block,f=f.block;g&&f&&!g.equals(f)&&b(f,e,d);e.remove();d.remove()}}},table:function(){function a(c){var d=[],g,f=new CKEDITOR.dom.walker(c),h=c.startPath().contains(e),m=c.endPath().contains(e),k={};f.guard=function(a,f){if(a.type==CKEDITOR.NODE_ELEMENT){var l="visited_"+(f?"out":"in");if(a.getCustomData(l))return;CKEDITOR.dom.element.setMarker(k,a,l,1)}if(f&&h&&a.equals(h))g=c.clone(),g.setEndAt(h,CKEDITOR.POSITION_BEFORE_END),d.push(g);else if(!f&&m&& a.equals(m))g=c.clone(),g.setStartAt(m,CKEDITOR.POSITION_AFTER_START),d.push(g);else{if(l=!f)l=a.type==CKEDITOR.NODE_ELEMENT&&a.is(e)&&(!h||b(a,h))&&(!m||b(a,m));if(!l&&(l=f))if(a.is(e))var l=h&&h.getAscendant("table",!0),n=m&&m.getAscendant("table",!0),q=a.getAscendant("table",!0),l=l&&l.contains(q)||n&&n.contains(q);else l=void 0;l&&(g=c.clone(),g.selectNodeContents(a),d.push(g))}};f.lastForward();CKEDITOR.dom.element.clearAllMarkers(k);return d}function b(a,c){var e=CKEDITOR.POSITION_CONTAINS+ CKEDITOR.POSITION_IS_CONTAINED,d=a.getPosition(c);return d===CKEDITOR.POSITION_IDENTICAL?!1:0===(d&e)}var e={td:1,th:1,caption:1};return{detectPurge:function(a){var b=a.range,c=b.clone();c.enlarge(CKEDITOR.ENLARGE_ELEMENT);var c=new CKEDITOR.dom.walker(c),d=0;c.evaluator=function(a){a.type==CKEDITOR.NODE_ELEMENT&&a.is(e)&&++d};c.checkForward();if(1g&&d&&d.intersectsNode(c.$)){var f=[{node:e.anchorNode,offset:e.anchorOffset},{node:e.focusNode,offset:e.focusOffset}];e.anchorNode==c.$&&e.anchorOffset>g&&(f[0].offset-=g);e.focusNode==c.$&&e.focusOffset>g&&(f[1].offset-=g)}}c.setText(q(c.getText(),1));f&&(c=a.getDocument().$, e=c.getSelection(),c=c.createRange(),c.setStart(f[0].node,f[0].offset),c.collapse(!0),e.removeAllRanges(),e.addRange(c),e.extend(f[1].node,f[1].offset))}}function q(a,b){return b?a.replace(z,function(a,b){return b?" ":""}):a.replace(r,"")}function y(a,b){var c=b&&CKEDITOR.tools.htmlEncode(b)||"\x26nbsp;",c=CKEDITOR.dom.element.createFromHtml('\x3cdiv data-cke-hidden-sel\x3d"1" data-cke-temp\x3d"1" style\x3d"'+(CKEDITOR.env.ie&&14>CKEDITOR.env.version?"display:none":"position:fixed;top:0;left:-1000px;width:0;height:0;overflow:hidden;")+ '"\x3e'+c+"\x3c/div\x3e",a.document);a.fire("lockSnapshot");a.editable().append(c);var e=a.getSelection(1),d=a.createRange(),g=e.root.on("selectionchange",function(a){a.cancel()},null,null,0);d.setStartAt(c,CKEDITOR.POSITION_AFTER_START);d.setEndAt(c,CKEDITOR.POSITION_BEFORE_END);e.selectRanges([d]);g.removeListener();a.fire("unlockSnapshot");a._.hiddenSelectionContainer=c}function u(a){var b={37:1,39:1,8:1,46:1};return function(c){var e=c.data.getKeystroke();if(b[e]){var d=a.getSelection().getRanges(), g=d[0];1==d.length&&g.collapsed&&(e=g[38>e?"getPreviousEditableNode":"getNextEditableNode"]())&&e.type==CKEDITOR.NODE_ELEMENT&&"false"==e.getAttribute("contenteditable")&&(a.getSelection().fake(e),c.data.preventDefault(),c.cancel())}}}function p(a){for(var b=0;b=e.getLength()?h.setStartAfter(e):h.setStartBefore(e));d&&d.type==CKEDITOR.NODE_TEXT&&(f?h.setEndAfter(d):h.setEndBefore(d));e=new CKEDITOR.dom.walker(h);e.evaluator=function(e){if(e.type==CKEDITOR.NODE_ELEMENT&&e.isReadOnly()){var d=c.clone();c.setEndBefore(e);c.collapsed&&a.splice(b--,1);e.getPosition(h.endContainer)& CKEDITOR.POSITION_CONTAINS||(d.setStartAfter(e),d.collapsed||a.splice(b+1,0,d));return!0}return!1};e.next()}}return a}var v="function"!=typeof window.getSelection,w=1,r=CKEDITOR.tools.repeat("​",7),z=new RegExp(r+"( )?","g"),t,x,A,B=CKEDITOR.dom.walker.invisible(1),C=function(){function a(b){return function(a){var c=a.editor.createRange();c.moveToClosestEditablePosition(a.selected,b)&&a.editor.getSelection().selectRanges([c]);return!1}}function b(a){return function(b){var c=b.editor,e=c.createRange(), d;if(!c.readOnly)return(d=e.moveToClosestEditablePosition(b.selected,a))||(d=e.moveToClosestEditablePosition(b.selected,!a)),d&&c.getSelection().selectRanges([e]),c.fire("saveSnapshot"),b.selected.remove(),d||(e.moveToElementEditablePosition(c.editable()),c.getSelection().selectRanges([e])),c.fire("saveSnapshot"),!1}}var c=a(),e=a(1);return{37:c,38:c,39:e,40:e,8:b(),46:b(1)}}();CKEDITOR.on("instanceCreated",function(a){function b(){var a=c.getSelection();a&&a.removeAllRanges()}var c=a.editor;c.on("contentDom", function(){function a(){t=new CKEDITOR.dom.selection(c.getSelection());t.lock()}function b(){g.removeListener("mouseup",b);m.removeListener("mouseup",b);var a=CKEDITOR.document.$.selection,c=a.createRange();"None"!=a.type&&c.parentElement()&&c.parentElement().ownerDocument==d.$&&c.select()}function e(a){a=a.getRanges()[0];return a?(a=a.startContainer.getAscendant(function(a){return a.type==CKEDITOR.NODE_ELEMENT&&a.hasAttribute("contenteditable")},!0))&&"false"===a.getAttribute("contenteditable")? a:null:null}var d=c.document,g=CKEDITOR.document,f=c.editable(),h=d.getBody(),m=d.getDocumentElement(),q=f.isInline(),r,t;CKEDITOR.env.gecko&&f.attachListener(f,"focus",function(a){a.removeListener();0!==r&&(a=c.getSelection().getNative())&&a.isCollapsed&&a.anchorNode==f.$&&(a=c.createRange(),a.moveToElementEditStart(f),a.select())},null,null,-2);f.attachListener(f,CKEDITOR.env.webkit||CKEDITOR.env.gecko?"focusin":"focus",function(){if(r&&(CKEDITOR.env.webkit||CKEDITOR.env.gecko)){r=c._.previousActive&& c._.previousActive.equals(d.getActive());var a=null!=c._.previousScrollTop&&c._.previousScrollTop!=f.$.scrollTop;CKEDITOR.env.webkit&&r&&a&&(f.$.scrollTop=c._.previousScrollTop)}c.unlockSelection(r);r=0},null,null,-1);f.attachListener(f,"mousedown",function(){r=0});if(CKEDITOR.env.ie||q)v?f.attachListener(f,"beforedeactivate",a,null,null,-1):f.attachListener(c,"selectionCheck",a,null,null,-1),f.attachListener(f,CKEDITOR.env.webkit||CKEDITOR.env.gecko?"focusout":"blur",function(){c.lockSelection(t); r=1},null,null,-1),f.attachListener(f,"mousedown",function(){r=0});if(CKEDITOR.env.ie&&!q){var w;f.attachListener(f,"mousedown",function(a){2==a.data.$.button&&((a=c.document.getSelection())&&a.getType()!=CKEDITOR.SELECTION_NONE||(w=c.window.getScrollPosition()))});f.attachListener(f,"mouseup",function(a){2==a.data.$.button&&w&&(c.document.$.documentElement.scrollLeft=w.x,c.document.$.documentElement.scrollTop=w.y);w=null});if("BackCompat"!=d.$.compatMode){if(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat){var z, y;m.on("mousedown",function(a){function b(a){a=a.data.$;if(z){var c=h.$.createTextRange();try{c.moveToPoint(a.clientX,a.clientY)}catch(e){}z.setEndPoint(0>y.compareEndPoints("StartToStart",c)?"EndToEnd":"StartToStart",c);z.select()}}function c(){m.removeListener("mousemove",b);g.removeListener("mouseup",c);m.removeListener("mouseup",c);z.select()}a=a.data;if(a.getTarget().is("html")&&a.$.yCKEDITOR.env.version)m.on("mousedown",function(a){a.data.getTarget().is("html")&&(g.on("mouseup",b),m.on("mouseup",b))})}}f.attachListener(f,"selectionchange",l,c);f.attachListener(f,"keyup",k,c);f.attachListener(f,"touchstart",k,c);f.attachListener(f,"touchend",k,c);CKEDITOR.env.ie&&f.attachListener(f,"keydown",function(a){var b=this.getSelection(1),c=e(b);c&&!c.equals(f)&&(b.selectElement(c),a.data.preventDefault())}, c);f.attachListener(f,CKEDITOR.env.webkit||CKEDITOR.env.gecko?"focusin":"focus",function(){c.forceNextSelectionCheck();c.selectionChange(1)});if(q&&(CKEDITOR.env.webkit||CKEDITOR.env.gecko)){var p;f.attachListener(f,"mousedown",function(){p=1});f.attachListener(d.getDocumentElement(),"mouseup",function(){p&&k.call(c);p=0})}else f.attachListener(CKEDITOR.env.ie?f:d.getDocumentElement(),"mouseup",k,c);CKEDITOR.env.webkit&&f.attachListener(d,"keydown",function(a){switch(a.data.getKey()){case 13:case 33:case 34:case 35:case 36:case 37:case 39:case 8:case 45:case 46:f.hasFocus&& n(f)}},null,null,-1);f.attachListener(f,"keydown",u(c),null,null,-1)});c.on("setData",function(){c.unlockSelection();CKEDITOR.env.webkit&&b()});c.on("contentDomUnload",function(){c.unlockSelection()});if(CKEDITOR.env.ie9Compat)c.on("beforeDestroy",b,null,null,9);c.on("dataReady",function(){delete c._.fakeSelection;delete c._.hiddenSelectionContainer;c.selectionChange(1)});c.on("loadSnapshot",function(){var a=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_ELEMENT),b=c.editable().getLast(a);b&&b.hasAttribute("data-cke-hidden-sel")&& (b.remove(),CKEDITOR.env.gecko&&(a=c.editable().getFirst(a))&&a.is("br")&&a.getAttribute("_moz_editor_bogus_node")&&a.remove())},null,null,100);c.on("key",function(a){if("wysiwyg"==c.mode){var b=c.getSelection();if(b.isFake){var e=C[a.data.keyCode];if(e)return e({editor:c,selected:b.getSelectedElement(),selection:b,keyEvent:a})}}})});if(CKEDITOR.env.webkit)CKEDITOR.on("instanceReady",function(a){var b=a.editor;b.on("selectionChange",function(){var a=b.editable(),c=a.getCustomData("cke-fillingChar"); c&&(c.getCustomData("ready")?(n(a),a.editor.fire("selectionCheck")):c.setCustomData("ready",1))},null,null,-1);b.on("beforeSetMode",function(){n(b.editable())},null,null,-1);b.on("getSnapshot",function(a){a.data&&(a.data=q(a.data))},b,null,20);b.on("toDataFormat",function(a){a.data.dataValue=q(a.data.dataValue)},null,null,0)});CKEDITOR.editor.prototype.selectionChange=function(a){(a?l:k).call(this)};CKEDITOR.editor.prototype.getSelection=function(a){return!this._.savedSelection&&!this._.fakeSelection|| a?(a=this.editable())&&"wysiwyg"==this.mode?new CKEDITOR.dom.selection(a):null:this._.savedSelection||this._.fakeSelection};CKEDITOR.editor.prototype.lockSelection=function(a){a=a||this.getSelection(1);return a.getType()!=CKEDITOR.SELECTION_NONE?(!a.isLocked&&a.lock(),this._.savedSelection=a,!0):!1};CKEDITOR.editor.prototype.unlockSelection=function(a){var b=this._.savedSelection;return b?(b.unlock(a),delete this._.savedSelection,!0):!1};CKEDITOR.editor.prototype.forceNextSelectionCheck=function(){delete this._.selectionPreviousPath}; CKEDITOR.dom.document.prototype.getSelection=function(){return new CKEDITOR.dom.selection(this)};CKEDITOR.dom.range.prototype.select=function(){var a=this.root instanceof CKEDITOR.editable?this.root.editor.getSelection():new CKEDITOR.dom.selection(this.root);a.selectRanges([this]);return a};CKEDITOR.SELECTION_NONE=1;CKEDITOR.SELECTION_TEXT=2;CKEDITOR.SELECTION_ELEMENT=3;CKEDITOR.dom.selection=function(a){if(a instanceof CKEDITOR.dom.selection){var b=a;a=a.root}var c=a instanceof CKEDITOR.dom.element; this.rev=b?b.rev:w++;this.document=a instanceof CKEDITOR.dom.document?a:a.getDocument();this.root=c?a:this.document.getBody();this.isLocked=0;this._={cache:{}};if(b)return CKEDITOR.tools.extend(this._.cache,b._.cache),this.isFake=b.isFake,this.isLocked=b.isLocked,this;a=this.getNative();var e,d;if(a)if(a.getRangeAt)e=(d=a.rangeCount&&a.getRangeAt(0))&&new CKEDITOR.dom.node(d.commonAncestorContainer);else{try{d=a.createRange()}catch(g){}e=d&&CKEDITOR.dom.element.get(d.item&&d.item(0)||d.parentElement())}if(!e|| e.type!=CKEDITOR.NODE_ELEMENT&&e.type!=CKEDITOR.NODE_TEXT||!this.root.equals(e)&&!this.root.contains(e))this._.cache.type=CKEDITOR.SELECTION_NONE,this._.cache.startElement=null,this._.cache.selectedElement=null,this._.cache.selectedText="",this._.cache.ranges=new CKEDITOR.dom.rangeList;return this};var H={img:1,hr:1,li:1,table:1,tr:1,td:1,th:1,embed:1,object:1,ol:1,ul:1,a:1,input:1,form:1,select:1,textarea:1,button:1,fieldset:1,thead:1,tfoot:1};CKEDITOR.tools.extend(CKEDITOR.dom.selection,{_removeFillingCharSequenceString:q, _createFillingCharSequenceNode:e,FILLING_CHAR_SEQUENCE:r});CKEDITOR.dom.selection.prototype={getNative:function(){return void 0!==this._.cache.nativeSel?this._.cache.nativeSel:this._.cache.nativeSel=v?this.document.$.selection:this.document.getWindow().$.getSelection()},getType:v?function(){var a=this._.cache;if(a.type)return a.type;var b=CKEDITOR.SELECTION_NONE;try{var c=this.getNative(),e=c.type;"Text"==e&&(b=CKEDITOR.SELECTION_TEXT);"Control"==e&&(b=CKEDITOR.SELECTION_ELEMENT);c.createRange().parentElement()&& (b=CKEDITOR.SELECTION_TEXT)}catch(d){}return a.type=b}:function(){var a=this._.cache;if(a.type)return a.type;var b=CKEDITOR.SELECTION_TEXT,c=this.getNative();if(!c||!c.rangeCount)b=CKEDITOR.SELECTION_NONE;else if(1==c.rangeCount){var c=c.getRangeAt(0),e=c.startContainer;e==c.endContainer&&1==e.nodeType&&1==c.endOffset-c.startOffset&&H[e.childNodes[c.startOffset].nodeName.toLowerCase()]&&(b=CKEDITOR.SELECTION_ELEMENT)}return a.type=b},getRanges:function(){var a=v?function(){function a(b){return(new CKEDITOR.dom.node(b)).getIndex()} var b=function(b,c){b=b.duplicate();b.collapse(c);var e=b.parentElement();if(!e.hasChildNodes())return{container:e,offset:0};for(var d=e.children,g,f,h=b.duplicate(),m=0,k=d.length-1,l=-1,n,q;m<=k;)if(l=Math.floor((m+k)/2),g=d[l],h.moveToElementText(g),n=h.compareEndPoints("StartToStart",b),0n)m=l+1;else return{container:e,offset:a(g)};if(-1==l||l==d.length-1&&0>n){h.moveToElementText(e);h.setEndPoint("StartToStart",b);h=h.text.replace(/(\r\n|\r)/g,"\n").length;d=e.childNodes;if(!h)return g= d[d.length-1],g.nodeType!=CKEDITOR.NODE_TEXT?{container:e,offset:d.length}:{container:g,offset:g.nodeValue.length};for(e=d.length;0]*>)[ \t\r\n]*/gi,"$1");f=f.replace(/([ \t\n\r]+| )/g, " ");f=f.replace(/]*>/gi,"\n");if(CKEDITOR.env.ie){var h=a.getDocument().createElement("div");h.append(g);g.$.outerHTML="\x3cpre\x3e"+f+"\x3c/pre\x3e";g.copyAttributes(h.getFirst());g=h.getFirst().remove()}else g.setHtml(f);b=g}else f?b=q(c?[a.getHtml()]:e(a),b):a.moveChildren(b);b.replace(a);if(d){var c=b,m;(m=c.getPrevious(E))&&m.type==CKEDITOR.NODE_ELEMENT&&m.is("pre")&&(d=n(m.getHtml(),/\n$/,"")+"\n\n"+n(c.getHtml(),/^\n/,""),CKEDITOR.env.ie?c.$.outerHTML="\x3cpre\x3e"+d+"\x3c/pre\x3e": c.setHtml(d),m.remove())}else c&&v(b)}function e(a){var b=[];n(a.getOuterHtml(),/(\S\s*)\n(?:\s|(]+data-cke-bookmark.*?\/span>))*\n(?!$)/gi,function(a,b,c){return b+"\x3c/pre\x3e"+c+"\x3cpre\x3e"}).replace(/([\s\S]*?)<\/pre>/gi,function(a,c){b.push(c)});return b}function n(a,b,c){var e="",d="";a=a.replace(/(^]+data-cke-bookmark.*?\/span>)|(]+data-cke-bookmark.*?\/span>$)/gi,function(a,b,c){b&&(e=b);c&&(d=c);return""});return e+a.replace(b,c)+d}function q(a,b){var c; 1=c?(l=d.createText(""),l.insertAfter(this)):(a=d.createText(""),a.insertAfter(l),a.remove()));return l},substring:function(a,f){return"number"!=typeof f?this.$.nodeValue.substr(a):this.$.nodeValue.substring(a,f)}}),function(){function a(a,c,d){var f=a.serializable,k=c[d?"endContainer":"startContainer"],g=d?"endOffset":"startOffset",h=f?c.document.getById(a.startNode):a.startNode;a=f?c.document.getById(a.endNode):a.endNode;k.equals(h.getPrevious())? (c.startOffset=c.startOffset-k.getLength()-a.getPrevious().getLength(),k=a.getNext()):k.equals(a.getPrevious())&&(c.startOffset-=k.getLength(),k=a.getNext());k.equals(h.getParent())&&c[g]++;k.equals(a.getParent())&&c[g]++;c[d?"endContainer":"startContainer"]=k;return c}CKEDITOR.dom.rangeList=function(a){if(a instanceof CKEDITOR.dom.rangeList)return a;a?a instanceof CKEDITOR.dom.range&&(a=[a]):a=[];return CKEDITOR.tools.extend(a,f)};var f={createIterator:function(){var a=this,c=CKEDITOR.dom.walker.bookmark(), d=[],f;return{getNextRange:function(k){f=void 0===f?0:f+1;var g=a[f];if(g&&1b?-1:1}),g=0,f;gCKEDITOR.env.version?a[h].$.styleSheet.cssText+=f:a[h].$.innerHTML+=f}}var l={};CKEDITOR.skin={path:a,loadPart:function(c, e){CKEDITOR.skin.name!=CKEDITOR.skinName.split(",")[0]?CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(a()+"skin.js"),function(){b(c,e)}):b(c,e)},getPath:function(a){return CKEDITOR.getUrl(f(a))},icons:{},addIcon:function(a,b,c,d){a=a.toLowerCase();this.icons[a]||(this.icons[a]={path:b,offset:c||0,bgsize:d||"16px"})},getIconStyle:function(a,b,c,d,g){var f;a&&(a=a.toLowerCase(),b&&(f=this.icons[a+"-rtl"]),f||(f=this.icons[a]));a=c||f&&f.path||"";d=d||f&&f.offset;g=g||f&&f.bgsize||"16px";a&&(a=a.replace(/'/g, "\\'"));return a&&"background-image:url('"+CKEDITOR.getUrl(a)+"');background-position:0 "+d+"px;background-size:"+g+";"}};CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{getUiColor:function(){return this.uiColor},setUiColor:function(a){var b=c(CKEDITOR.document);return(this.setUiColor=function(a){this.uiColor=a;var c=CKEDITOR.skin.chameleon,f="",m="";"function"==typeof c&&(f=c(this,"editor"),m=c(this,"panel"));a=[[h,a]];d([b],f,a);d(g,m,a)}).call(this,a)}});var k="cke_ui_color",g=[],h=/\$color/g; CKEDITOR.on("instanceLoaded",function(a){if(!CKEDITOR.env.ie||!CKEDITOR.env.quirks){var b=a.editor;a=function(a){a=(a.data[0]||a.data).element.getElementsByTag("iframe").getItem(0).getFrameDocument();if(!a.getById("cke_ui_color")){var f=c(a);g.push(f);b.on("destroy",function(){g=CKEDITOR.tools.array.filter(g,function(a){return f!==a})});(a=b.getUiColor())&&d([f],CKEDITOR.skin.chameleon(b,"panel"),[[h,a]])}};b.on("panelShow",a);b.on("menuShow",a);b.config.uiColor&&b.setUiColor(b.config.uiColor)}})}(), function(){if(CKEDITOR.env.webkit)CKEDITOR.env.hc=!1;else{var a=CKEDITOR.dom.element.createFromHtml('\x3cdiv style\x3d"width:0;height:0;position:absolute;left:-10000px;border:1px solid;border-color:red blue"\x3e\x3c/div\x3e',CKEDITOR.document);a.appendTo(CKEDITOR.document.getHead());try{var f=a.getComputedStyle("border-top-color"),b=a.getComputedStyle("border-right-color");CKEDITOR.env.hc=!(!f||f!=b)}catch(c){CKEDITOR.env.hc=!1}a.remove()}CKEDITOR.env.hc&&(CKEDITOR.env.cssClass+=" cke_hc");CKEDITOR.document.appendStyleText(".cke{visibility:hidden;}"); CKEDITOR.status="loaded";CKEDITOR.fireOnce("loaded");if(a=CKEDITOR._.pending)for(delete CKEDITOR._.pending,f=0;ff;f++){var k=f,g;g=parseInt(d[f],16);g=("0"+(0>c?0|g*(1+c):0| g+(255-g)*c).toString(16)).slice(-2);d[k]=g}return"#"+d.join("")}}(),f={editor:new CKEDITOR.template("{id}.cke_chrome [border-color:{defaultBorder};] {id} .cke_top [ background-color:{defaultBackground};border-bottom-color:{defaultBorder};] {id} .cke_bottom [background-color:{defaultBackground};border-top-color:{defaultBorder};] {id} .cke_resizer [border-right-color:{ckeResizer}] {id} .cke_dialog_title [background-color:{defaultBackground};border-bottom-color:{defaultBorder};] {id} .cke_dialog_footer [background-color:{defaultBackground};outline-color:{defaultBorder};] {id} .cke_dialog_tab [background-color:{dialogTab};border-color:{defaultBorder};] {id} .cke_dialog_tab:hover [background-color:{lightBackground};] {id} .cke_dialog_contents [border-top-color:{defaultBorder};] {id} .cke_dialog_tab_selected, {id} .cke_dialog_tab_selected:hover [background:{dialogTabSelected};border-bottom-color:{dialogTabSelectedBorder};] {id} .cke_dialog_body [background:{dialogBody};border-color:{defaultBorder};] {id} a.cke_button_off:hover,{id} a.cke_button_off:focus,{id} a.cke_button_off:active [background-color:{darkBackground};border-color:{toolbarElementsBorder};] {id} .cke_button_on [background-color:{ckeButtonOn};border-color:{toolbarElementsBorder};] {id} .cke_toolbar_separator,{id} .cke_toolgroup a.cke_button:last-child:after,{id} .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after [background-color: {toolbarElementsBorder};border-color: {toolbarElementsBorder};] {id} a.cke_combo_button:hover,{id} a.cke_combo_button:focus,{id} .cke_combo_on a.cke_combo_button [border-color:{toolbarElementsBorder};background-color:{darkBackground};] {id} .cke_combo:after [border-color:{toolbarElementsBorder};] {id} .cke_path_item [color:{elementsPathColor};] {id} a.cke_path_item:hover,{id} a.cke_path_item:focus,{id} a.cke_path_item:active [background-color:{darkBackground};] {id}.cke_panel [border-color:{defaultBorder};] "), panel:new CKEDITOR.template(".cke_panel_grouptitle [background-color:{lightBackground};border-color:{defaultBorder};] .cke_menubutton_icon [background-color:{menubuttonIcon};] .cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active [background-color:{menubuttonHover};] .cke_menubutton:hover .cke_menubutton_icon, .cke_menubutton:focus .cke_menubutton_icon, .cke_menubutton:active .cke_menubutton_icon [background-color:{menubuttonIconHover};] .cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon [background-color:{menubuttonIcon};] .cke_menuseparator [background-color:{menubuttonIcon};] a:hover.cke_colorbox, a:active.cke_colorbox [border-color:{defaultBorder};] a:hover.cke_colorauto, a:hover.cke_colormore, a:active.cke_colorauto, a:active.cke_colormore [background-color:{ckeColorauto};border-color:{defaultBorder};] ")}; return function(b,c){var d=a(b.uiColor,.4),d={id:"."+b.id,defaultBorder:a(d,-.2),toolbarElementsBorder:a(d,-.25),defaultBackground:d,lightBackground:a(d,.8),darkBackground:a(d,-.15),ckeButtonOn:a(d,.4),ckeResizer:a(d,-.4),ckeColorauto:a(d,.8),dialogBody:a(d,.7),dialogTab:a(d,.65),dialogTabSelected:"#FFF",dialogTabSelectedBorder:"#FFF",elementsPathColor:a(d,-.6),menubuttonHover:a(d,.1),menubuttonIcon:a(d,.5),menubuttonIconHover:a(d,.3)};return f[c].output(d).replace(/\[/g,"{").replace(/\]/g,"}")}}(), CKEDITOR.plugins.add("dialogui",{onLoad:function(){var a=function(a){this._||(this._={});this._["default"]=this._.initValue=a["default"]||"";this._.required=a.required||!1;for(var b=[this._],c=1;carguments.length)){var g=a.call(this,c);g.labelId=CKEDITOR.tools.getNextId()+ "_label";this._.children=[];var f={role:c.role||"presentation"};c.includeLabel&&(f["aria-labelledby"]=g.labelId);CKEDITOR.ui.dialog.uiElement.call(this,b,c,e,"div",null,f,function(){var a=[],e=c.required?" cke_required":"";"horizontal"!=c.labelLayout?a.push('\x3clabel class\x3d"cke_dialog_ui_labeled_label'+e+'" ',' id\x3d"'+g.labelId+'"',g.inputId?' for\x3d"'+g.inputId+'"':"",(c.labelStyle?' style\x3d"'+c.labelStyle+'"':"")+"\x3e",c.label,"\x3c/label\x3e",'\x3cdiv class\x3d"cke_dialog_ui_labeled_content"', c.controlStyle?' style\x3d"'+c.controlStyle+'"':"",' role\x3d"presentation"\x3e',d.call(this,b,c),"\x3c/div\x3e"):(e={type:"hbox",widths:c.widths,padding:0,children:[{type:"html",html:'\x3clabel class\x3d"cke_dialog_ui_labeled_label'+e+'" id\x3d"'+g.labelId+'" for\x3d"'+g.inputId+'"'+(c.labelStyle?' style\x3d"'+c.labelStyle+'"':"")+"\x3e"+CKEDITOR.tools.htmlEncode(c.label)+"\x3c/label\x3e"},{type:"html",html:'\x3cspan class\x3d"cke_dialog_ui_labeled_content"'+(c.controlStyle?' style\x3d"'+c.controlStyle+ '"':"")+"\x3e"+d.call(this,b,c)+"\x3c/span\x3e"}]},CKEDITOR.dialog._.uiElementBuilders.hbox.build(b,e,a));return a.join("")})}},textInput:function(b,c,e){if(!(3>arguments.length)){a.call(this,c);var d=this._.inputId=CKEDITOR.tools.getNextId()+"_textInput",f={"class":"cke_dialog_ui_input_"+c.type,id:d,type:c.type};c.validate&&(this.validate=c.validate);c.maxLength&&(f.maxlength=c.maxLength);c.size&&(f.size=c.size);c.inputStyle&&(f.style=c.inputStyle);var k=this,l=!1;b.on("load",function(){k.getInputElement().on("keydown", function(a){13==a.data.getKeystroke()&&(l=!0)});k.getInputElement().on("keyup",function(a){13==a.data.getKeystroke()&&l&&(b.getButton("ok")&&setTimeout(function(){b.getButton("ok").click()},0),l=!1);k.bidi&&g.call(k,a)},null,null,1E3)});CKEDITOR.ui.dialog.labeledElement.call(this,b,c,e,function(){var a=['\x3cdiv class\x3d"cke_dialog_ui_input_',c.type,'" role\x3d"presentation"'];c.width&&a.push('style\x3d"width:'+c.width+'" ');a.push("\x3e\x3cinput ");f["aria-labelledby"]=this._.labelId;this._.required&& (f["aria-required"]=this._.required);for(var b in f)a.push(b+'\x3d"'+f[b]+'" ');a.push(" /\x3e\x3c/div\x3e");return a.join("")})}},textarea:function(b,c,e){if(!(3>arguments.length)){a.call(this,c);var d=this,f=this._.inputId=CKEDITOR.tools.getNextId()+"_textarea",k={};c.validate&&(this.validate=c.validate);k.rows=c.rows||5;k.cols=c.cols||20;k["class"]="cke_dialog_ui_input_textarea "+(c["class"]||"");"undefined"!=typeof c.inputStyle&&(k.style=c.inputStyle);c.dir&&(k.dir=c.dir);if(d.bidi)b.on("load", function(){d.getInputElement().on("keyup",g)},d);CKEDITOR.ui.dialog.labeledElement.call(this,b,c,e,function(){k["aria-labelledby"]=this._.labelId;this._.required&&(k["aria-required"]=this._.required);var a=['\x3cdiv class\x3d"cke_dialog_ui_input_textarea" role\x3d"presentation"\x3e\x3ctextarea id\x3d"',f,'" '],b;for(b in k)a.push(b+'\x3d"'+CKEDITOR.tools.htmlEncode(k[b])+'" ');a.push("\x3e",CKEDITOR.tools.htmlEncode(d._["default"]),"\x3c/textarea\x3e\x3c/div\x3e");return a.join("")})}},checkbox:function(b, c,e){if(!(3>arguments.length)){var d=a.call(this,c,{"default":!!c["default"]});c.validate&&(this.validate=c.validate);CKEDITOR.ui.dialog.uiElement.call(this,b,c,e,"span",null,null,function(){var a=CKEDITOR.tools.extend({},c,{id:c.id?c.id+"_checkbox":CKEDITOR.tools.getNextId()+"_checkbox"},!0),e=[],g=CKEDITOR.tools.getNextId()+"_label",f={"class":"cke_dialog_ui_checkbox_input",type:"checkbox","aria-labelledby":g};k(a);c["default"]&&(f.checked="checked");"undefined"!=typeof a.inputStyle&&(a.style=a.inputStyle); d.checkbox=new CKEDITOR.ui.dialog.uiElement(b,a,e,"input",null,f);e.push(' \x3clabel id\x3d"',g,'" for\x3d"',f.id,'"'+(c.labelStyle?' style\x3d"'+c.labelStyle+'"':"")+"\x3e",CKEDITOR.tools.htmlEncode(c.label),"\x3c/label\x3e");return e.join("")})}},radio:function(b,c,e){if(!(3>arguments.length)){a.call(this,c);this._["default"]||(this._["default"]=this._.initValue=c.items[0][1]);c.validate&&(this.validate=c.validate);var d=[],g=this;c.role="radiogroup";c.includeLabel=!0;CKEDITOR.ui.dialog.labeledElement.call(this, b,c,e,function(){for(var a=[],e=[],f=(c.id?c.id:CKEDITOR.tools.getNextId())+"_radio",l=0;larguments.length)){var d=a.call(this,c);c.validate&&(this.validate=c.validate);d.inputId=CKEDITOR.tools.getNextId()+"_select";CKEDITOR.ui.dialog.labeledElement.call(this,b,c,e,function(){var a=CKEDITOR.tools.extend({},c,{id:c.id?c.id+"_select":CKEDITOR.tools.getNextId()+"_select"},!0),e=[],g=[],f={id:d.inputId,"class":"cke_dialog_ui_input_select","aria-labelledby":this._.labelId};e.push('\x3cdiv class\x3d"cke_dialog_ui_input_', c.type,'" role\x3d"presentation"');c.width&&e.push('style\x3d"width:'+c.width+'" ');e.push("\x3e");void 0!==c.size&&(f.size=c.size);void 0!==c.multiple&&(f.multiple=c.multiple);k(a);for(var l=0,w;larguments.length)){void 0===c["default"]&&(c["default"]="");var d=CKEDITOR.tools.extend(a.call(this,c),{definition:c,buttons:[]});c.validate&&(this.validate=c.validate);b.on("load",function(){CKEDITOR.document.getById(d.frameId).getParent().addClass("cke_dialog_ui_input_file")});CKEDITOR.ui.dialog.labeledElement.call(this,b,c,e,function(){d.frameId=CKEDITOR.tools.getNextId()+"_fileInput";var a=['\x3ciframe frameborder\x3d"0" allowtransparency\x3d"0" class\x3d"cke_dialog_ui_input_file" role\x3d"presentation" id\x3d"', d.frameId,'" title\x3d"',c.label,'" src\x3d"javascript:void('];a.push(CKEDITOR.env.ie?"(function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.close();")+"})()":"0");a.push(')"\x3e\x3c/iframe\x3e');return a.join("")})}},fileButton:function(b,c,e){var d=this;if(!(3>arguments.length)){a.call(this,c);c.validate&&(this.validate=c.validate);var g=CKEDITOR.tools.extend({},c),f=g.onClick;g.className=(g.className?g.className+" ":"")+"cke_dialog_ui_button";g.onClick=function(a){var e= c["for"];a=f?f.call(this,a):!1;!1!==a&&("xhr"!==a&&b.getContentElement(e[0],e[1]).submit(),this.disable())};b.on("load",function(){b.getContentElement(c["for"][0],c["for"][1])._.buttons.push(d)});CKEDITOR.ui.dialog.button.call(this,b,g,e)}},html:function(){var a=/^\s*<[\w:]+\s+([^>]*)?>/,b=/^(\s*<[\w:]+(?:\s+[^>]*)?)((?:.|\r|\n)+)$/,c=/\/$/;return function(d,g,f){if(!(3>arguments.length)){var k=[],l=g.html;"\x3c"!=l.charAt(0)&&(l="\x3cspan\x3e"+l+"\x3c/span\x3e");var v=g.focus;if(v){var w=this.focus; this.focus=function(){("function"==typeof v?v:w).call(this);this.fire("focus")};g.isFocusable&&(this.isFocusable=this.isFocusable);this.keyboardFocusable=!0}CKEDITOR.ui.dialog.uiElement.call(this,d,g,k,"span",null,null,"");k=k.join("").match(a);l=l.match(b)||["","",""];c.test(l[1])&&(l[1]=l[1].slice(0,-1),l[2]="/"+l[2]);f.push([l[1]," ",k[1]||"",l[2]].join(""))}}}(),fieldset:function(a,b,c,d,g){var f=g.label;this._={children:b};CKEDITOR.ui.dialog.uiElement.call(this,a,g,d,"fieldset",null,null,function(){var a= [];f&&a.push("\x3clegend"+(g.labelStyle?' style\x3d"'+g.labelStyle+'"':"")+"\x3e"+f+"\x3c/legend\x3e");for(var b=0;bb.getChildCount()?(new CKEDITOR.dom.text(a,CKEDITOR.document)).appendTo(b):b.getChild(0).$.nodeValue= a;return this},getLabel:function(){var a=CKEDITOR.document.getById(this._.labelId);return!a||1>a.getChildCount()?"":a.getChild(0).getText()},eventProcessors:d},!0);CKEDITOR.ui.dialog.button.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{click:function(){return this._.disabled?!1:this.fire("click",{dialog:this._.dialog})},enable:function(){this._.disabled=!1;var a=this.getElement();a&&a.removeClass("cke_disabled")},disable:function(){this._.disabled=!0;this.getElement().addClass("cke_disabled")}, isVisible:function(){return this.getElement().getFirst().isVisible()},isEnabled:function(){return!this._.disabled},eventProcessors:CKEDITOR.tools.extend({},CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors,{onClick:function(a,b){this.on("click",function(){b.apply(this,arguments)})}},!0),accessKeyUp:function(){this.click()},accessKeyDown:function(){this.focus()},keyboardFocusable:!0},!0);CKEDITOR.ui.dialog.textInput.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,{getInputElement:function(){return CKEDITOR.document.getById(this._.inputId)}, focus:function(){var a=this.selectParentTab();setTimeout(function(){var b=a.getInputElement();b&&b.$.focus()},0)},select:function(){var a=this.selectParentTab();setTimeout(function(){var b=a.getInputElement();b&&(b.$.focus(),b.$.select())},0)},accessKeyUp:function(){this.select()},setValue:function(a){if(this.bidi){var b=a&&a.charAt(0);(b="‪"==b?"ltr":"‫"==b?"rtl":null)&&(a=a.slice(1));this.setDirectionMarker(b)}a||(a="");return CKEDITOR.ui.dialog.uiElement.prototype.setValue.apply(this,arguments)}, getValue:function(){var a=CKEDITOR.ui.dialog.uiElement.prototype.getValue.call(this);if(this.bidi&&a){var b=this.getDirectionMarker();b&&(a=("ltr"==b?"‪":"‫")+a)}return a},setDirectionMarker:function(a){var b=this.getInputElement();a?b.setAttributes({dir:a,"data-cke-dir-marker":a}):this.getDirectionMarker()&&b.removeAttributes(["dir","data-cke-dir-marker"])},getDirectionMarker:function(){return this.getInputElement().data("cke-dir-marker")},keyboardFocusable:!0},c,!0);CKEDITOR.ui.dialog.textarea.prototype= new CKEDITOR.ui.dialog.textInput;CKEDITOR.ui.dialog.select.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,{getInputElement:function(){return this._.select.getElement()},add:function(a,b,c){var d=new CKEDITOR.dom.element("option",this.getDialog().getParentEditor().document),g=this.getInputElement().$;d.$.text=a;d.$.value=void 0===b||null===b?a:b;void 0===c||null===c?CKEDITOR.env.ie?g.add(d.$):g.add(d.$,null):g.add(d.$,c);return this},remove:function(a){this.getInputElement().$.remove(a); return this},clear:function(){for(var a=this.getInputElement().$;0b-a;c--)if(this._.tabs[this._.tabIdList[c%a]][0].$.offsetHeight)return this._.tabIdList[c%a];return null}function f(){for(var a=this._.tabIdList.length,b=CKEDITOR.tools.indexOf(this._.tabIdList,this._.currentTabId),c=b+1;cl.width-k.width-f?l.width-k.width+("rtl"==g.lang.dir?0:h[1]):d.x,d.y+h[0]l.height-k.height-f?l.height-k.height+h[2]:d.y,1);c.data.preventDefault()} function c(){CKEDITOR.document.removeListener("mousemove",b);CKEDITOR.document.removeListener("mouseup",c);if(CKEDITOR.env.ie6Compat){var a=B.getChild(0).getFrameDocument();a.removeListener("mousemove",b);a.removeListener("mouseup",c)}}var e=null,d=null,g=a.getParentEditor(),f=g.config.dialog_magnetDistance,h=CKEDITOR.skin.margins||[0,0,0,0];"undefined"==typeof f&&(f=20);a.parts.title.on("mousedown",function(g){e={x:g.data.$.screenX,y:g.data.$.screenY};CKEDITOR.document.on("mousemove",b);CKEDITOR.document.on("mouseup", c);d=a.getPosition();if(CKEDITOR.env.ie6Compat){var f=B.getChild(0).getFrameDocument();f.on("mousemove",b);f.on("mouseup",c)}g.data.preventDefault()},a)}function e(a){function b(c){var n="rtl"==g.lang.dir,r=m.width,t=m.height,v=r+(c.data.$.screenX-l.x)*(n?-1:1)*(a._.moved?1:2),q=t+(c.data.$.screenY-l.y)*(a._.moved?1:2),w=a._.element.getFirst(),w=n&&w.getComputedStyle("right"),z=a.getPosition();z.y+q>k.height&&(q=k.height-z.y);(n?w:z.x)+v>k.width&&(v=k.width-(n?w:z.x));if(d==CKEDITOR.DIALOG_RESIZE_WIDTH|| d==CKEDITOR.DIALOG_RESIZE_BOTH)r=Math.max(e.minWidth||0,v-f);if(d==CKEDITOR.DIALOG_RESIZE_HEIGHT||d==CKEDITOR.DIALOG_RESIZE_BOTH)t=Math.max(e.minHeight||0,q-h);a.resize(r,t);a._.moved||a.layout();c.data.preventDefault()}function c(){CKEDITOR.document.removeListener("mouseup",c);CKEDITOR.document.removeListener("mousemove",b);n&&(n.remove(),n=null);if(CKEDITOR.env.ie6Compat){var a=B.getChild(0).getFrameDocument();a.removeListener("mouseup",c);a.removeListener("mousemove",b)}}var e=a.definition,d=e.resizable; if(d!=CKEDITOR.DIALOG_RESIZE_NONE){var g=a.getParentEditor(),f,h,k,l,m,n,r=CKEDITOR.tools.addFunction(function(e){m=a.getSize();var d=a.parts.contents;d.$.getElementsByTagName("iframe").length&&(n=CKEDITOR.dom.element.createFromHtml('\x3cdiv class\x3d"cke_dialog_resize_cover" style\x3d"height: 100%; position: absolute; width: 100%;"\x3e\x3c/div\x3e'),d.append(n));h=m.height-a.parts.contents.getSize("height",!(CKEDITOR.env.gecko||CKEDITOR.env.ie&&CKEDITOR.env.quirks));f=m.width-a.parts.contents.getSize("width", 1);l={x:e.screenX,y:e.screenY};k=CKEDITOR.document.getWindow().getViewPaneSize();CKEDITOR.document.on("mousemove",b);CKEDITOR.document.on("mouseup",c);CKEDITOR.env.ie6Compat&&(d=B.getChild(0).getFrameDocument(),d.on("mousemove",b),d.on("mouseup",c));e.preventDefault&&e.preventDefault()});a.on("load",function(){var b="";d==CKEDITOR.DIALOG_RESIZE_WIDTH?b=" cke_resizer_horizontal":d==CKEDITOR.DIALOG_RESIZE_HEIGHT&&(b=" cke_resizer_vertical");b=CKEDITOR.dom.element.createFromHtml('\x3cdiv class\x3d"cke_resizer'+ b+" cke_resizer_"+g.lang.dir+'" title\x3d"'+CKEDITOR.tools.htmlEncode(g.lang.common.resize)+'" onmousedown\x3d"CKEDITOR.tools.callFunction('+r+', event )"\x3e'+("ltr"==g.lang.dir?"◢":"◣")+"\x3c/div\x3e");a.parts.footer.append(b,1)});g.on("destroy",function(){CKEDITOR.tools.removeFunction(r)})}}function n(a){a.data.preventDefault(1)}function q(a){var b=CKEDITOR.document.getWindow(),c=a.config,e=CKEDITOR.skinName||a.config.skin,d=c.dialog_backgroundCoverColor||("moono-lisa"==e?"black":"white"),e=c.dialog_backgroundCoverOpacity, g=c.baseFloatZIndex,c=CKEDITOR.tools.genKey(d,e,g),f=A[c];f?f.show():(g=['\x3cdiv tabIndex\x3d"-1" style\x3d"position: ',CKEDITOR.env.ie6Compat?"absolute":"fixed","; z-index: ",g,"; top: 0px; left: 0px; ",CKEDITOR.env.ie6Compat?"":"background-color: "+d,'" class\x3d"cke_dialog_background_cover"\x3e'],CKEDITOR.env.ie6Compat&&(d="\x3chtml\x3e\x3cbody style\x3d\\'background-color:"+d+";\\'\x3e\x3c/body\x3e\x3c/html\x3e",g.push('\x3ciframe hidefocus\x3d"true" frameborder\x3d"0" id\x3d"cke_dialog_background_iframe" src\x3d"javascript:'), g.push("void((function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.write( '"+d+"' );document.close();")+"})())"),g.push('" style\x3d"position:absolute;left:0;top:0;width:100%;height: 100%;filter: progid:DXImageTransform.Microsoft.Alpha(opacity\x3d0)"\x3e\x3c/iframe\x3e')),g.push("\x3c/div\x3e"),f=CKEDITOR.dom.element.createFromHtml(g.join("")),f.setOpacity(void 0!==e?e:.5),f.on("keydown",n),f.on("keypress",n),f.on("keyup",n),f.appendTo(CKEDITOR.document.getBody()), A[c]=f);a.focusManager.add(f);B=f;a=function(){var a=b.getViewPaneSize();f.setStyles({width:a.width+"px",height:a.height+"px"})};var h=function(){var a=b.getScrollPosition(),c=CKEDITOR.dialog._.currentTop;f.setStyles({left:a.x+"px",top:a.y+"px"});if(c){do a=c.getPosition(),c.move(a.x,a.y);while(c=c._.parentDialog)}};x=a;b.on("resize",a);a();CKEDITOR.env.mac&&CKEDITOR.env.webkit||f.focus();if(CKEDITOR.env.ie6Compat){var k=function(){h();k.prevScrollHandler.apply(this,arguments)};b.$.setTimeout(function(){k.prevScrollHandler= window.onscroll||function(){};window.onscroll=k},0);h()}}function y(a){B&&(a.focusManager.remove(B),a=CKEDITOR.document.getWindow(),B.hide(),B=null,a.removeListener("resize",x),CKEDITOR.env.ie6Compat&&a.$.setTimeout(function(){window.onscroll=window.onscroll&&window.onscroll.prevScrollHandler||null},0),x=null)}var u=CKEDITOR.tools.cssLength,p='\x3cdiv class\x3d"cke_reset_all {editorId} {editorDialogClass} {hidpi}" dir\x3d"{langDir}" lang\x3d"{langCode}" role\x3d"dialog" aria-labelledby\x3d"cke_dialog_title_{id}"\x3e\x3ctable class\x3d"cke_dialog '+ CKEDITOR.env.cssClass+' cke_{langDir}" style\x3d"position:absolute" role\x3d"presentation"\x3e\x3ctr\x3e\x3ctd role\x3d"presentation"\x3e\x3cdiv class\x3d"cke_dialog_body" role\x3d"presentation"\x3e\x3cdiv id\x3d"cke_dialog_title_{id}" class\x3d"cke_dialog_title" role\x3d"presentation"\x3e\x3c/div\x3e\x3ca id\x3d"cke_dialog_close_button_{id}" class\x3d"cke_dialog_close_button" href\x3d"javascript:void(0)" title\x3d"{closeTitle}" role\x3d"button"\x3e\x3cspan class\x3d"cke_label"\x3eX\x3c/span\x3e\x3c/a\x3e\x3cdiv id\x3d"cke_dialog_tabs_{id}" class\x3d"cke_dialog_tabs" role\x3d"tablist"\x3e\x3c/div\x3e\x3ctable class\x3d"cke_dialog_contents" role\x3d"presentation"\x3e\x3ctr\x3e\x3ctd id\x3d"cke_dialog_contents_{id}" class\x3d"cke_dialog_contents_body" role\x3d"presentation"\x3e\x3c/td\x3e\x3c/tr\x3e\x3ctr\x3e\x3ctd id\x3d"cke_dialog_footer_{id}" class\x3d"cke_dialog_footer" role\x3d"presentation"\x3e\x3c/td\x3e\x3c/tr\x3e\x3c/table\x3e\x3c/div\x3e\x3c/td\x3e\x3c/tr\x3e\x3c/table\x3e\x3c/div\x3e'; CKEDITOR.dialog=function(b,g){function h(){var a=x._.focusList;a.sort(function(a,b){return a.tabIndex!=b.tabIndex?b.tabIndex-a.tabIndex:a.focusIndex-b.focusIndex});for(var b=a.length,c=0;cb.length)){var c=x._.currentFocusIndex;x._.tabBarMode&&0>a&&(c=0);try{b[c].getInputElement().$.blur()}catch(e){}var d=c,g=1c.height||b.width+(0c.width?a.setStyle("position","absolute"):a.setStyle("position","fixed"));this.move(this._.moved?this._.position.x:e,this._.moved?this._.position.y:d)},foreach:function(a){for(var b in this._.contents)for(var c in this._.contents[b])a.call(this,this._.contents[b][c]);return this},reset:function(){var a=function(a){a.reset&&a.reset(1)};return function(){this.foreach(a);return this}}(), setupContent:function(){var a=arguments;this.foreach(function(b){b.setup&&b.setup.apply(b,a)})},commitContent:function(){var a=arguments;this.foreach(function(b){CKEDITOR.env.ie&&this._.currentFocusIndex==b.focusIndex&&b.getInputElement().$.blur();b.commit&&b.commit.apply(b,a)})},hide:function(){if(this.parts.dialog.isVisible()){this.fire("hide",{});this._.editor.fire("dialogHide",this);this.selectPage(this._.tabIdList[0]);var a=this._.element;a.setStyle("display","none");this.parts.dialog.setStyle("visibility", "hidden");for(J(this);CKEDITOR.dialog._.currentTop!=this;)CKEDITOR.dialog._.currentTop.hide();if(this._.parentDialog){var b=this._.parentDialog.getElement().getFirst();b.setStyle("z-index",parseInt(b.$.style.zIndex,10)+Math.floor(this._.editor.config.baseFloatZIndex/2))}else y(this._.editor);if(CKEDITOR.dialog._.currentTop=this._.parentDialog)CKEDITOR.dialog._.currentZIndex-=10;else{CKEDITOR.dialog._.currentZIndex=null;a.removeListener("keydown",H);a.removeListener("keyup",F);var c=this._.editor; c.focus();setTimeout(function(){c.focusManager.unlock();CKEDITOR.env.iOS&&c.window.focus()},0)}delete this._.parentDialog;this.foreach(function(a){a.resetInitValue&&a.resetInitValue()});this.setState(CKEDITOR.DIALOG_STATE_IDLE)}},addPage:function(a){if(!a.requiredContent||this._.editor.filter.check(a.requiredContent)){for(var b=[],c=a.label?' title\x3d"'+CKEDITOR.tools.htmlEncode(a.label)+'"':"",e=CKEDITOR.dialog._.uiElementBuilders.vbox.build(this,{type:"vbox",className:"cke_dialog_page_contents", children:a.elements,expand:!!a.expand,padding:a.padding,style:a.style||"width: 100%;"},b),d=this._.contents[a.id]={},g=e.getChild(),f=0;e=g.shift();)e.notAllowed||"hbox"==e.type||"vbox"==e.type||f++,d[e.id]=e,"function"==typeof e.getChild&&g.push.apply(g,e.getChild());f||(a.hidden=!0);b=CKEDITOR.dom.element.createFromHtml(b.join(""));b.setAttribute("role","tabpanel");e=CKEDITOR.env;d="cke_"+a.id+"_"+CKEDITOR.tools.getNextNumber();c=CKEDITOR.dom.element.createFromHtml(['\x3ca class\x3d"cke_dialog_tab"', 0arguments.length)){var h=(e.call?e(b):e)||"div",k=["\x3c",h," "],l=(d&&d.call?d(b):d)||{},m=(g&&g.call?g(b):g)||{},n=(f&&f.call?f.call(this,a,b):f)||"",r=this.domId=m.id||CKEDITOR.tools.getNextId()+"_uiElement";b.requiredContent&&!a.getParentEditor().filter.check(b.requiredContent)&&(l.display="none",this.notAllowed=!0);m.id=r;var q={};b.type&&(q["cke_dialog_ui_"+ b.type]=1);b.className&&(q[b.className]=1);b.disabled&&(q.cke_disabled=1);for(var t=m["class"]&&m["class"].split?m["class"].split(" "):[],r=0;rCKEDITOR.env.version?"cke_dialog_ui_focused":"";b.on("focus",function(){a._.tabBarMode=!1;a._.hasFocus=!0;v.fire("focus"); c&&this.addClass(c)});b.on("blur",function(){v.fire("blur");c&&this.removeClass(c)})}});CKEDITOR.tools.extend(this,b);this.keyboardFocusable&&(this.tabIndex=b.tabIndex||0,this.focusIndex=a._.focusList.push(this)-1,this.on("focus",function(){a._.currentFocusIndex=v.focusIndex}))}},hbox:function(a,b,c,e,d){if(!(4>arguments.length)){this._||(this._={});var g=this._.children=b,f=d&&d.widths||null,h=d&&d.height||null,k,l={role:"presentation"};d&&d.align&&(l.align=d.align);CKEDITOR.ui.dialog.uiElement.call(this, a,d||{type:"hbox"},e,"table",{},l,function(){var a=['\x3ctbody\x3e\x3ctr class\x3d"cke_dialog_ui_hbox"\x3e'];for(k=0;karguments.length)){this._||(this._={});var g=this._.children=b,f=d&&d.width||null,h=d&&d.heights||null;CKEDITOR.ui.dialog.uiElement.call(this,a,d||{type:"vbox"},e,"div",null,{role:"presentation"},function(){var b=['\x3ctable role\x3d"presentation" cellspacing\x3d"0" border\x3d"0" ']; b.push('style\x3d"');d&&d.expand&&b.push("height:100%;");b.push("width:"+u(f||"100%"),";");CKEDITOR.env.webkit&&b.push("float:none;");b.push('"');b.push('align\x3d"',CKEDITOR.tools.htmlEncode(d&&d.align||("ltr"==a.getParentEditor().lang.dir?"left":"right")),'" ');b.push("\x3e\x3ctbody\x3e");for(var e=0;earguments.length)return this._.children.concat();a.splice||(a=[a]);return 2>a.length?this._.children[a[0]]:this._.children[a[0]]&&this._.children[a[0]].getChild?this._.children[a[0]].getChild(a.slice(1,a.length)):null}},!0);CKEDITOR.ui.dialog.vbox.prototype=new CKEDITOR.ui.dialog.hbox;(function(){var a={build:function(a,b,c){for(var e=b.children,d,g=[],f=[],h=0;hk.length&&(b=a.document.createElement(a.config.enterMode==CKEDITOR.ENTER_P?"p":"div"),g=l.shift(),d.insertNode(b),b.append(new CKEDITOR.dom.text("",a.document)),d.moveToBookmark(g),d.selectNodeContents(b),d.collapse(!0),g=d.createBookmark(),k.push(b),l.unshift(g)); h=k[0].getParent();d=[];for(g=0;gc||(this.notifications.splice(c,1),a.element.remove(), this.element.getChildCount()||(this._removeListeners(),this.element.remove()))},_createElement:function(){var a=this.editor,c=a.config,d=new CKEDITOR.dom.element("div");d.addClass("cke_notifications_area");d.setAttribute("id","cke_notifications_area_"+a.name);d.setStyle("z-index",c.baseFloatZIndex-2);return d},_attachListeners:function(){var a=CKEDITOR.document.getWindow(),c=this.editor;a.on("scroll",this._uiBuffer.input);a.on("resize",this._uiBuffer.input);c.on("change",this._changeBuffer.input); c.on("floatingSpaceLayout",this._layout,this,null,20);c.on("blur",this._layout,this,null,20)},_removeListeners:function(){var a=CKEDITOR.document.getWindow(),c=this.editor;a.removeListener("scroll",this._uiBuffer.input);a.removeListener("resize",this._uiBuffer.input);c.removeListener("change",this._changeBuffer.input);c.removeListener("floatingSpaceLayout",this._layout);c.removeListener("blur",this._layout)},_layout:function(){function a(){c.setStyle("left",w(r+f.width-n-q))}var c=this.element,d= this.editor,f=d.ui.contentsElement.getClientRect(),k=d.ui.contentsElement.getDocumentPosition(),g,h,m=c.getClientRect(),e,n=this._notificationWidth,q=this._notificationMargin;e=CKEDITOR.document.getWindow();var y=e.getScrollPosition(),u=e.getViewPaneSize(),p=CKEDITOR.document.getBody(),v=p.getDocumentPosition(),w=CKEDITOR.tools.cssLength;n&&q||(e=this.element.getChild(0),n=this._notificationWidth=e.getClientRect().width,q=this._notificationMargin=parseInt(e.getComputedStyle("margin-left"),10)+parseInt(e.getComputedStyle("margin-right"), 10));d.toolbar&&(g=d.ui.space("top"),h=g.getClientRect());g&&g.isVisible()&&h.bottom>f.top&&h.bottomy.y?c.setStyles({position:"fixed",top:0}):c.setStyles({position:"absolute",top:w(k.y+f.height-m.height)});var r="fixed"==c.getStyle("position")?f.left:"static"!=p.getComputedStyle("position")?k.x-v.x:k.x;f.widthy.x+u.width?a():c.setStyle("left", w(r)):k.x+n+q>y.x+u.width?c.setStyle("left",w(r)):k.x+f.width/2+n/2+q>y.x+u.width?c.setStyle("left",w(r-k.x+y.x+u.width-n-q)):0>f.left+f.width-n-q?a():0>f.left+f.width/2-n/2?c.setStyle("left",w(r-k.x+y.x)):c.setStyle("left",w(r+f.width/2-n/2-q/2))}};CKEDITOR.plugins.notification=a}(),function(){var a='\x3ca id\x3d"{id}" class\x3d"cke_button cke_button__{name} cke_button_{state} {cls}"'+(CKEDITOR.env.gecko&&!CKEDITOR.env.hc?"":" href\x3d\"javascript:void('{titleJs}')\"")+' title\x3d"{title}" tabindex\x3d"-1" hidefocus\x3d"true" role\x3d"button" aria-labelledby\x3d"{id}_label" aria-describedby\x3d"{id}_description" aria-haspopup\x3d"{hasArrow}" aria-disabled\x3d"{ariaDisabled}"'; CKEDITOR.env.gecko&&CKEDITOR.env.mac&&(a+=' onkeypress\x3d"return false;"');CKEDITOR.env.gecko&&(a+=' onblur\x3d"this.style.cssText \x3d this.style.cssText;"');var f="";CKEDITOR.env.ie&&(f='return false;" onmouseup\x3d"CKEDITOR.tools.getMouseButton(event)\x3d\x3dCKEDITOR.MOUSE_BUTTON_LEFT\x26\x26');var a=a+(' onkeydown\x3d"return CKEDITOR.tools.callFunction({keydownFn},event);" onfocus\x3d"return CKEDITOR.tools.callFunction({focusFn},event);" onclick\x3d"'+f+'CKEDITOR.tools.callFunction({clickFn},this);return false;"\x3e\x3cspan class\x3d"cke_button_icon cke_button__{iconName}_icon" style\x3d"{style}"')+ '\x3e\x26nbsp;\x3c/span\x3e\x3cspan id\x3d"{id}_label" class\x3d"cke_button_label cke_button__{name}_label" aria-hidden\x3d"false"\x3e{label}\x3c/span\x3e\x3cspan id\x3d"{id}_description" class\x3d"cke_button_label" aria-hidden\x3d"false"\x3e{ariaShortcut}\x3c/span\x3e{arrowHtml}\x3c/a\x3e',b=CKEDITOR.addTemplate("buttonArrow",'\x3cspan class\x3d"cke_button_arrow"\x3e'+(CKEDITOR.env.hc?"\x26#9660;":"")+"\x3c/span\x3e"),c=CKEDITOR.addTemplate("button",a);CKEDITOR.plugins.add("button",{beforeInit:function(a){a.ui.addHandler(CKEDITOR.UI_BUTTON, CKEDITOR.ui.button.handler)}});CKEDITOR.UI_BUTTON="button";CKEDITOR.ui.button=function(a){CKEDITOR.tools.extend(this,a,{title:a.label,click:a.click||function(b){b.execCommand(a.command)}});this._={}};CKEDITOR.ui.button.handler={create:function(a){return new CKEDITOR.ui.button(a)}};CKEDITOR.ui.button.prototype={render:function(a,f){function k(){var b=a.mode;b&&(b=this.modes[b]?void 0!==g[b]?g[b]:CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,b=a.readOnly&&!this.readOnly?CKEDITOR.TRISTATE_DISABLED: b,this.setState(b),this.refresh&&this.refresh())}var g=null,h=CKEDITOR.env,m=this._.id=CKEDITOR.tools.getNextId(),e="",n=this.command,q,y,u;this._.editor=a;var p={id:m,button:this,editor:a,focus:function(){CKEDITOR.document.getById(m).focus()},execute:function(){this.button.click(a)},attach:function(a){this.button.attach(a)}},v=CKEDITOR.tools.addFunction(function(a){if(p.onkey)return a=new CKEDITOR.dom.event(a),!1!==p.onkey(p,a.getKeystroke())}),w=CKEDITOR.tools.addFunction(function(a){var b;p.onfocus&& (b=!1!==p.onfocus(p,new CKEDITOR.dom.event(a)));return b}),r=0;p.clickFn=q=CKEDITOR.tools.addFunction(function(){r&&(a.unlockSelection(1),r=0);p.execute();h.iOS&&a.focus()});this.modes?(g={},a.on("beforeModeUnload",function(){a.mode&&this._.state!=CKEDITOR.TRISTATE_DISABLED&&(g[a.mode]=this._.state)},this),a.on("activeFilterChange",k,this),a.on("mode",k,this),!this.readOnly&&a.on("readOnly",k,this)):n&&(n=a.getCommand(n))&&(n.on("state",function(){this.setState(n.state)},this),e+=n.state==CKEDITOR.TRISTATE_ON? "on":n.state==CKEDITOR.TRISTATE_DISABLED?"disabled":"off");var z;if(this.directional)a.on("contentDirChanged",function(b){var c=CKEDITOR.document.getById(this._.id),e=c.getFirst();b=b.data;b!=a.lang.dir?c.addClass("cke_"+b):c.removeClass("cke_ltr").removeClass("cke_rtl");e.setAttribute("style",CKEDITOR.skin.getIconStyle(z,"rtl"==b,this.icon,this.iconOffset))},this);n?(y=a.getCommandKeystroke(n))&&(u=CKEDITOR.tools.keystrokeToString(a.lang.common.keyboard,y)):e+="off";y=this.name||this.command;var t= null,x=this.icon;z=y;this.icon&&!/\./.test(this.icon)?(z=this.icon,x=null):(this.icon&&(t=this.icon),CKEDITOR.env.hidpi&&this.iconHiDpi&&(t=this.iconHiDpi));t?(CKEDITOR.skin.addIcon(t,t),x=null):t=z;e={id:m,name:y,iconName:z,label:this.label,cls:(this.hasArrow?"cke_button_expandable ":"")+(this.className||""),state:e,ariaDisabled:"disabled"==e?"true":"false",title:this.title+(u?" ("+u.display+")":""),ariaShortcut:u?a.lang.common.keyboardShortcut+" "+u.aria:"",titleJs:h.gecko&&!h.hc?"":(this.title|| "").replace("'",""),hasArrow:"string"===typeof this.hasArrow&&this.hasArrow||(this.hasArrow?"true":"false"),keydownFn:v,focusFn:w,clickFn:q,style:CKEDITOR.skin.getIconStyle(t,"rtl"==a.lang.dir,x,this.iconOffset),arrowHtml:this.hasArrow?b.output():""};c.output(e,f);if(this.onRender)this.onRender();return p},setState:function(a){if(this._.state==a)return!1;this._.state=a;var b=CKEDITOR.document.getById(this._.id);return b?(b.setState(a,"cke_button"),b.setAttribute("aria-disabled",a==CKEDITOR.TRISTATE_DISABLED), this.hasArrow?b.setAttribute("aria-expanded",a==CKEDITOR.TRISTATE_ON):a===CKEDITOR.TRISTATE_ON?b.setAttribute("aria-pressed",!0):b.removeAttribute("aria-pressed"),!0):!1},getState:function(){return this._.state},toFeature:function(a){if(this._.feature)return this._.feature;var b=this;this.allowedContent||this.requiredContent||!this.command||(b=a.getCommand(this.command)||b);return this._.feature=b}};CKEDITOR.ui.prototype.addButton=function(a,b){this.add(a,CKEDITOR.UI_BUTTON,b)}}(),function(){function a(a){function b(){for(var e= c(),h=CKEDITOR.tools.clone(a.config.toolbarGroups)||f(a),m=0;mb.order?-1:0>a.order? 1:a.order]+data-cke-bookmark[^<]*?<\/span>/ig,"");g&&a(b,d)})}function A(){if("wysiwyg"==b.mode){var a=B("paste");b.getCommand("cut").setState(B("cut"));b.getCommand("copy").setState(B("copy"));b.getCommand("paste").setState(a);b.fire("pasteState",a)}}function B(a){var c= b.getSelection(),c=c&&c.getRanges()[0];if((b.readOnly||c&&c.checkReadOnly())&&a in{paste:1,cut:1})return CKEDITOR.TRISTATE_DISABLED;if("paste"==a)return CKEDITOR.TRISTATE_OFF;a=b.getSelection();c=a.getRanges();return a.getType()==CKEDITOR.SELECTION_NONE||1==c.length&&c[0].collapsed?CKEDITOR.TRISTATE_DISABLED:CKEDITOR.TRISTATE_OFF}var C=CKEDITOR.plugins.clipboard,H=0,F=0;(function(){b.on("key",t);b.on("contentDom",c);b.on("selectionChange",A);if(b.contextMenu){b.contextMenu.addListener(function(){return{cut:B("cut"), copy:B("copy"),paste:B("paste")}});var a=null;b.on("menuShow",function(){a&&(a.removeListener(),a=null);var c=b.contextMenu.findItemByCommandName("paste");c&&c.element&&(a=c.element.on("touchend",function(){b._.forcePasteDialog=!0}))})}if(b.ui.addButton)b.once("instanceReady",function(){b._.pasteButtons&&CKEDITOR.tools.array.forEach(b._.pasteButtons,function(a){if(a=b.ui.get(a))if(a=CKEDITOR.document.getById(a._.id))a.on("touchend",function(){b._.forcePasteDialog=!0})})})})();(function(){function a(c, d,g,f,h){var k=b.lang.clipboard[d];b.addCommand(d,g);b.ui.addButton&&b.ui.addButton(c,{label:k,command:d,toolbar:"clipboard,"+f});b.addMenuItems&&b.addMenuItem(d,{label:k,command:d,group:"clipboard",order:h})}a("Cut","cut",d("cut"),10,1);a("Copy","copy",d("copy"),20,4);a("Paste","paste",g(),30,8);b._.pasteButtons||(b._.pasteButtons=[]);b._.pasteButtons.push("Paste")})();b.getClipboardData=function(a,c){function d(a){a.removeListener();a.cancel();c(a.data)}function g(a){a.removeListener();a.cancel(); c({type:h,dataValue:a.data.dataValue,dataTransfer:a.data.dataTransfer,method:"paste"})}var f=!1,h="auto";c||(c=a,a=null);b.on("beforePaste",function(a){a.removeListener();f=!0;h=a.data.type},null,null,1E3);b.on("paste",d,null,null,0);!1===z()&&(b.removeListener("paste",d),b._.forcePasteDialog&&f&&b.fire("pasteDialog")?(b.on("pasteDialogCommit",g),b.on("dialogHide",function(a){a.removeListener();a.data.removeListener("pasteDialogCommit",g);a.data._.committed||c(null)})):c(null))}}function b(a){if(CKEDITOR.env.webkit){if(!a.match(/^[^<]*$/g)&& !a.match(/^(
<\/div>|
[^<]*<\/div>)*$/gi))return"html"}else if(CKEDITOR.env.ie){if(!a.match(/^([^<]|)*$/gi)&&!a.match(/^(

([^<]|)*<\/p>|(\r\n))*$/gi))return"html"}else if(CKEDITOR.env.gecko){if(!a.match(/^([^<]|)*$/gi))return"html"}else return"html";return"htmlifiedtext"}function c(a,b){function c(a){return CKEDITOR.tools.repeat("\x3c/p\x3e\x3cp\x3e",~~(a/2))+(1==a%2?"\x3cbr\x3e":"")}b=b.replace(/(?!\u3000)\s+/g," ").replace(/> +/gi, "\x3cbr\x3e");b=b.replace(/<\/?[A-Z]+>/g,function(a){return a.toLowerCase()});if(b.match(/^[^<]$/))return b;CKEDITOR.env.webkit&&-1(
|)<\/div>)(?!$|(

(
|)<\/div>))/g,"\x3cbr\x3e").replace(/^(
(
|)<\/div>){2}(?!$)/g,"\x3cdiv\x3e\x3c/div\x3e"),b.match(/
(
|)<\/div>/)&&(b="\x3cp\x3e"+b.replace(/(
(
|)<\/div>)+/g,function(a){return c(a.split("\x3c/div\x3e\x3cdiv\x3e").length+1)})+"\x3c/p\x3e"),b=b.replace(/<\/div>
/g,"\x3cbr\x3e"), b=b.replace(/<\/?div>/g,""));CKEDITOR.env.gecko&&a.enterMode!=CKEDITOR.ENTER_BR&&(CKEDITOR.env.gecko&&(b=b.replace(/^

$/,"\x3cbr\x3e")),-1){2,}/g,function(a){return c(a.length/4)})+"\x3c/p\x3e"));return k(a,b)}function d(a){function b(){var a={},c;for(c in CKEDITOR.dtd)"$"!=c.charAt(0)&&"div"!=c&&"span"!=c&&(a[c]=1);return a}var c={};return{get:function(d){return"plain-text"==d?c.plainText||(c.plainText=new CKEDITOR.filter(a, "br")):"semantic-content"==d?((d=c.semanticContent)||(d=new CKEDITOR.filter(a,{}),d.allow({$1:{elements:b(),attributes:!0,styles:!1,classes:!1}}),d=c.semanticContent=d),d):d?new CKEDITOR.filter(a,d):null}}}function l(a,b,c){b=CKEDITOR.htmlParser.fragment.fromHtml(b);var d=new CKEDITOR.htmlParser.basicWriter;c.applyTo(b,!0,!1,a.activeEnterMode);b.writeHtml(d);return d.getHtml()}function k(a,b){a.enterMode==CKEDITOR.ENTER_BR?b=b.replace(/(<\/p>

)+/g,function(a){return CKEDITOR.tools.repeat("\x3cbr\x3e", a.length/7*2)}).replace(/<\/?p>/g,""):a.enterMode==CKEDITOR.ENTER_DIV&&(b=b.replace(/<(\/)?p>/g,"\x3c$1div\x3e"));return b}function g(a){a.data.preventDefault();a.data.$.dataTransfer.dropEffect="none"}function h(b){var c=CKEDITOR.plugins.clipboard;b.on("contentDom",function(){function d(c,g,f){g.select();a(b,{dataTransfer:f,method:"drop"},1);f.sourceEditor.fire("saveSnapshot");f.sourceEditor.editable().extractHtmlFromRange(c);f.sourceEditor.getSelection().selectRanges([c]);f.sourceEditor.fire("saveSnapshot")} function g(d,f){d.select();a(b,{dataTransfer:f,method:"drop"},1);c.resetDragDataTransfer()}function f(a,c,d){var g={$:a.data.$,target:a.data.getTarget()};c&&(g.dragRange=c);d&&(g.dropRange=d);!1===b.fire(a.name,g)&&a.data.preventDefault()}function h(a){a.type!=CKEDITOR.NODE_ELEMENT&&(a=a.getParent());return a.getChildCount()}var k=b.editable(),l=CKEDITOR.plugins.clipboard.getDropTarget(b),m=b.ui.space("top"),z=b.ui.space("bottom");c.preventDefaultDropOnElement(m);c.preventDefaultDropOnElement(z); k.attachListener(l,"dragstart",f);k.attachListener(b,"dragstart",c.resetDragDataTransfer,c,null,1);k.attachListener(b,"dragstart",function(a){c.initDragDataTransfer(a,b)},null,null,2);k.attachListener(b,"dragstart",function(){var a=c.dragRange=b.getSelection().getRanges()[0];CKEDITOR.env.ie&&10>CKEDITOR.env.version&&(c.dragStartContainerChildCount=a?h(a.startContainer):null,c.dragEndContainerChildCount=a?h(a.endContainer):null)},null,null,100);k.attachListener(l,"dragend",f);k.attachListener(b,"dragend", c.initDragDataTransfer,c,null,1);k.attachListener(b,"dragend",c.resetDragDataTransfer,c,null,100);k.attachListener(l,"dragover",function(a){if(CKEDITOR.env.edge)a.data.preventDefault();else{var b=a.data.getTarget();b&&b.is&&b.is("html")?a.data.preventDefault():CKEDITOR.env.ie&&CKEDITOR.plugins.clipboard.isFileApiSupported&&a.data.$.dataTransfer.types.contains("Files")&&a.data.preventDefault()}});k.attachListener(l,"drop",function(a){if(!a.data.$.defaultPrevented){a.data.preventDefault();var d=a.data.getTarget(); if(!d.isReadOnly()||d.type==CKEDITOR.NODE_ELEMENT&&d.is("html")){var d=c.getRangeAtDropPosition(a,b),g=c.dragRange;d&&f(a,g,d)}}},null,null,9999);k.attachListener(b,"drop",c.initDragDataTransfer,c,null,1);k.attachListener(b,"drop",function(a){if(a=a.data){var f=a.dropRange,h=a.dragRange,k=a.dataTransfer;k.getTransferType(b)==CKEDITOR.DATA_TRANSFER_INTERNAL?setTimeout(function(){c.internalDrop(h,f,k,b)},0):k.getTransferType(b)==CKEDITOR.DATA_TRANSFER_CROSS_EDITORS?d(h,f,k):g(f,k)}},null,null,9999)})} var m;CKEDITOR.plugins.add("clipboard",{requires:"dialog,notification,toolbar",init:function(a){var g,k=d(a);a.config.forcePasteAsPlainText?g="plain-text":a.config.pasteFilter?g=a.config.pasteFilter:!CKEDITOR.env.webkit||"pasteFilter"in a.config||(g="semantic-content");a.pasteFilter=k.get(g);f(a);h(a);CKEDITOR.dialog.add("paste",CKEDITOR.getUrl(this.path+"dialogs/paste.js"));if(CKEDITOR.env.gecko){var m=["image/png","image/jpeg","image/gif"],u;a.on("paste",function(b){var c=b.data,d=c.dataTransfer; if(!c.dataValue&&"paste"==c.method&&d&&1==d.getFilesCount()&&u!=d.id&&(d=d.getFile(0),-1!=CKEDITOR.tools.indexOf(m,d.type))){var g=new FileReader;g.addEventListener("load",function(){b.data.dataValue='\x3cimg src\x3d"'+g.result+'" /\x3e';a.fire("paste",b.data)},!1);g.addEventListener("abort",function(){a.fire("paste",b.data)},!1);g.addEventListener("error",function(){a.fire("paste",b.data)},!1);g.readAsDataURL(d);u=c.dataTransfer.id;b.stop()}},null,null,1)}a.on("paste",function(b){b.data.dataTransfer|| (b.data.dataTransfer=new CKEDITOR.plugins.clipboard.dataTransfer);if(!b.data.dataValue){var c=b.data.dataTransfer,d=c.getData("text/html");if(d)b.data.dataValue=d,b.data.type="html";else if(d=c.getData("text/plain"))b.data.dataValue=a.editable().transformPlainTextToHtml(d),b.data.type="text"}},null,null,1);a.on("paste",function(a){var b=a.data.dataValue,c=CKEDITOR.dtd.$block;-1 <\/span>/gi," "),"html"!=a.data.type&&(b=b.replace(/]*>([^<]*)<\/span>/gi, function(a,b){return b.replace(/\t/g,"\x26nbsp;\x26nbsp; \x26nbsp;")})),-1/,"")),b=b.replace(/(<[^>]+) class="Apple-[^"]*"/gi,"$1"));if(b.match(/^<[^<]+cke_(editable|contents)/i)){var d,e,g=new CKEDITOR.dom.element("div");for(g.setHtml(b);1==g.getChildCount()&&(d=g.getFirst())&&d.type==CKEDITOR.NODE_ELEMENT&&(d.hasClass("cke_editable")|| d.hasClass("cke_contents"));)g=e=d;e&&(b=e.getHtml().replace(/
$/i,""))}CKEDITOR.env.ie?b=b.replace(/^ (?: |\r\n)?<(\w+)/g,function(b,d){return d.toLowerCase()in c?(a.data.preSniffing="html","\x3c"+d):b}):CKEDITOR.env.webkit?b=b.replace(/<\/(\w+)>


<\/div>$/,function(b,d){return d in c?(a.data.endsWithEOL=1,"\x3c/"+d+"\x3e"):b}):CKEDITOR.env.gecko&&(b=b.replace(/(\s)
$/,"$1"));a.data.dataValue=b},null,null,3);a.on("paste",function(d){d=d.data;var g=a._.nextPasteType||d.type,f=d.dataValue, h,m=a.config.clipboard_defaultContentType||"html",n=d.dataTransfer.getTransferType(a)==CKEDITOR.DATA_TRANSFER_EXTERNAL,u=!0===a.config.forcePasteAsPlainText;h="html"==g||"html"==d.preSniffing?"html":b(f);delete a._.nextPasteType;"htmlifiedtext"==h&&(f=c(a.config,f));if("text"==g&&"html"==h)f=l(a,f,k.get("plain-text"));else if(n&&a.pasteFilter&&!d.dontFilter||u)f=l(a,f,a.pasteFilter);d.startsWithEOL&&(f='\x3cbr data-cke-eol\x3d"1"\x3e'+f);d.endsWithEOL&&(f+='\x3cbr data-cke-eol\x3d"1"\x3e');"auto"== g&&(g="html"==h||"html"==m?"html":"text");d.type=g;d.dataValue=f;delete d.preSniffing;delete d.startsWithEOL;delete d.endsWithEOL},null,null,6);a.on("paste",function(b){b=b.data;b.dataValue&&(a.insertHtml(b.dataValue,b.type,b.range),setTimeout(function(){a.fire("afterPaste")},0))},null,null,1E3);a.on("pasteDialog",function(b){setTimeout(function(){a.openDialog("paste",b.data)},0)})}});CKEDITOR.plugins.clipboard={isCustomCopyCutSupported:(!CKEDITOR.env.ie||16<=CKEDITOR.env.version)&&!CKEDITOR.env.iOS, isCustomDataTypesSupported:!CKEDITOR.env.ie||16<=CKEDITOR.env.version,isFileApiSupported:!CKEDITOR.env.ie||9CKEDITOR.env.version||b.isInline()?b:a.document},fixSplitNodesAfterDrop:function(a,b,c,d){function g(a,c,d){var e=a;e.type==CKEDITOR.NODE_TEXT&&(e=a.getParent());if(e.equals(c)&&d!=c.getChildCount())return a=b.startContainer.getChild(b.startOffset-1),c=b.startContainer.getChild(b.startOffset), a&&a.type==CKEDITOR.NODE_TEXT&&c&&c.type==CKEDITOR.NODE_TEXT&&(d=a.getLength(),a.setText(a.getText()+c.getText()),c.remove(),b.setStart(a,d),b.collapse(!0)),!0}var f=b.startContainer;"number"==typeof d&&"number"==typeof c&&f.type==CKEDITOR.NODE_ELEMENT&&(g(a.startContainer,f,c)||g(a.endContainer,f,d))},isDropRangeAffectedByDragRange:function(a,b){var c=b.startContainer,d=b.endOffset;return a.endContainer.equals(c)&&a.endOffset<=d||a.startContainer.getParent().equals(c)&&a.startContainer.getIndex()< d||a.endContainer.getParent().equals(c)&&a.endContainer.getIndex()CKEDITOR.env.version&&this.fixSplitNodesAfterDrop(b,c,f.dragStartContainerChildCount,f.dragEndContainerChildCount);(l=this.isDropRangeAffectedByDragRange(b,c))||(k=b.createBookmark(!1));f=c.clone().createBookmark(!1);l&&(k=b.createBookmark(!1));b=k.startNode;c= k.endNode;l=f.startNode;c&&b.getPosition(l)&CKEDITOR.POSITION_PRECEDING&&c.getPosition(l)&CKEDITOR.POSITION_FOLLOWING&&l.insertBefore(b);b=g.createRange();b.moveToBookmark(k);h.extractHtmlFromRange(b,1);c=g.createRange();f.startNode.getCommonAncestor(h)||(f=g.getSelection().createBookmarks()[0]);c.moveToBookmark(f);a(g,{dataTransfer:d,method:"drop",range:c},1);g.fire("unlockSnapshot")},getRangeAtDropPosition:function(a,b){var c=a.data.$,d=c.clientX,g=c.clientY,f=b.getSelection(!0).getRanges()[0], h=b.createRange();if(a.data.testRange)return a.data.testRange;if(document.caretRangeFromPoint&&b.document.$.caretRangeFromPoint(d,g))c=b.document.$.caretRangeFromPoint(d,g),h.setStart(CKEDITOR.dom.node(c.startContainer),c.startOffset),h.collapse(!0);else if(c.rangeParent)h.setStart(CKEDITOR.dom.node(c.rangeParent),c.rangeOffset),h.collapse(!0);else{if(CKEDITOR.env.ie&&8l&&!k;l++){if(!k)try{c.moveToPoint(d,g-l),k=!0}catch(m){}if(!k)try{c.moveToPoint(d,g+l),k=!0}catch(t){}}if(k){var x="cke-temp-"+(new Date).getTime();c.pasteHTML('\x3cspan id\x3d"'+x+'"\x3e​\x3c/span\x3e');var A=b.document.getById(x);h.moveToPosition(A,CKEDITOR.POSITION_BEFORE_START);A.remove()}else{var B=b.document.$.elementFromPoint(d,g),C=new CKEDITOR.dom.element(B),H;if(C.equals(b.editable())||"html"==C.getName())return f&&f.startContainer&&!f.startContainer.equals(b.editable())? f:null;H=C.getClientRect();d/i,bodyRegExp:/([\s\S]*)<\/body>/i,fragmentRegExp:/\x3c!--(?:Start|End)Fragment--\x3e/g,data:{},files:[],nativeHtmlCache:"",normalizeType:function(a){a=a.toLowerCase();return"text"==a||"text/plain"==a?"Text":"url"==a?"URL":a}};this._.fallbackDataTransfer=new CKEDITOR.plugins.clipboard.fallbackDataTransfer(this);this.id=this.getData(m);this.id||(this.id="Text"==m?"":"cke-"+ CKEDITOR.tools.getUniqueId());b&&(this.sourceEditor=b,this.setData("text/html",b.getSelectedHtml(1)),"Text"==m||this.getData("text/plain")||this.setData("text/plain",b.getSelection().getSelectedText()))};CKEDITOR.DATA_TRANSFER_INTERNAL=1;CKEDITOR.DATA_TRANSFER_CROSS_EDITORS=2;CKEDITOR.DATA_TRANSFER_EXTERNAL=3;CKEDITOR.plugins.clipboard.dataTransfer.prototype={getData:function(a,b){a=this._.normalizeType(a);var c="text/html"==a&&b?this._.nativeHtmlCache:this._.data[a];if(void 0===c||null===c||""=== c){if(this._.fallbackDataTransfer.isRequired())c=this._.fallbackDataTransfer.getData(a,b);else try{c=this.$.getData(a)||""}catch(d){c=""}"text/html"!=a||b||(c=this._stripHtml(c))}"Text"==a&&CKEDITOR.env.gecko&&this.getFilesCount()&&"file://"==c.substring(0,7)&&(c="");if("string"===typeof c)var g=c.indexOf("\x3c/html\x3e"),c=-1!==g?c.substring(0,g+7):c;return c},setData:function(a,b){a=this._.normalizeType(a);"text/html"==a?(this._.data[a]=this._stripHtml(b),this._.nativeHtmlCache=b):this._.data[a]= b;if(CKEDITOR.plugins.clipboard.isCustomDataTypesSupported||"URL"==a||"Text"==a)if("Text"==m&&"Text"==a&&(this.id=b),this._.fallbackDataTransfer.isRequired())this._.fallbackDataTransfer.setData(a,b);else try{this.$.setData(a,b)}catch(c){}},storeId:function(){"Text"!==m&&this.setData(m,this.id)},getTransferType:function(a){return this.sourceEditor?this.sourceEditor==a?CKEDITOR.DATA_TRANSFER_INTERNAL:CKEDITOR.DATA_TRANSFER_CROSS_EDITORS:CKEDITOR.DATA_TRANSFER_EXTERNAL},cacheData:function(){function a(c){c= b._.normalizeType(c);var d=b.getData(c);"text/html"==c&&(b._.nativeHtmlCache=b.getData(c,!0),d=b._stripHtml(d));d&&(b._.data[c]=d)}if(this.$){var b=this,c,d;if(CKEDITOR.plugins.clipboard.isCustomDataTypesSupported){if(this.$.types)for(c=0;cc?v+c:b.width>c?v-a.left:v-a.right+b.width):ec?v-c:b.width>c?v-a.right+b.width:v-a.left);c=a.top;b.height-a.topd?w-d:b.height>d?w-a.bottom+b.height:w-a.top);CKEDITOR.env.ie&&!CKEDITOR.env.edge&&(b=a=new CKEDITOR.dom.element(n.$.offsetParent),"html"==b.getName()&&(b=b.getDocument().getBody()),"rtl"==b.getComputedStyle("direction")&&(v=CKEDITOR.env.ie8Compat?v-2*n.getDocument().getDocumentElement().$.scrollLeft:v-(a.$.scrollWidth- a.$.clientWidth)));var a=n.getFirst(),k;(k=a.getCustomData("activePanel"))&&k.onHide&&k.onHide.call(this,1);a.setCustomData("activePanel",this);n.setStyles({top:w+"px",left:v+"px"});n.setOpacity(1);g&&g()},this);h.isLoaded?a():h.onLoad=a;CKEDITOR.tools.setTimeout(function(){var a=CKEDITOR.env.webkit&&CKEDITOR.document.getWindow().getScrollPosition().y;this.focus();m.element.focus();CKEDITOR.env.webkit&&(CKEDITOR.document.getBody().$.scrollTop=a);this.allowBlur(!0);this._.markFirst&&(CKEDITOR.env.ie? CKEDITOR.tools.setTimeout(function(){m.markFirstDisplayed?m.markFirstDisplayed():m._.markFirstDisplayed()},0):m.markFirstDisplayed?m.markFirstDisplayed():m._.markFirstDisplayed());this._.editor.fire("panelShow",this)},0,this)},CKEDITOR.env.air?200:0,this);this.visible=1;this.onShow&&this.onShow.call(this)},reposition:function(){var a=this._.showBlockParams;this.visible&&this._.showBlockParams&&(this.hide(),this.showBlock.apply(this,a))},focus:function(){if(CKEDITOR.env.webkit){var a=CKEDITOR.document.getActive(); a&&!a.equals(this._.iframe)&&a.$.blur()}(this._.lastFocused||this._.iframe.getFrameDocument().getWindow()).focus()},blur:function(){var a=this._.iframe.getFrameDocument().getActive();a&&a.is("a")&&(this._.lastFocused=a)},hide:function(a){if(this.visible&&(!this.onHide||!0!==this.onHide.call(this))){this.hideChild();CKEDITOR.env.gecko&&this._.iframe.getFrameDocument().$.activeElement.blur();this.element.setStyle("display","none");this.visible=0;this.element.getFirst().removeCustomData("activePanel"); if(a=a&&this._.returnFocus)CKEDITOR.env.webkit&&a.type&&a.getWindow().$.focus(),a.focus();delete this._.lastFocused;this._.showBlockParams=null;this._.editor.fire("panelHide",this)}},allowBlur:function(a){var c=this._.panel;void 0!==a&&(c.allowBlur=a);return c.allowBlur},showAsChild:function(a,c,d,f,k,g){if(this._.activeChild!=a||a._.panel._.offsetParentId!=d.getId())this.hideChild(),a.onHide=CKEDITOR.tools.bind(function(){CKEDITOR.tools.setTimeout(function(){this._.focused||this.hide()},0,this)}, this),this._.activeChild=a,this._.focused=!1,a.showBlock(c,d,f,k,g),this.blur(),(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat)&&setTimeout(function(){a.element.getChild(0).$.style.cssText+=""},100)},hideChild:function(a){var c=this._.activeChild;c&&(delete c.onHide,delete this._.activeChild,c.hide(),a&&this.focus())}}});CKEDITOR.on("instanceDestroyed",function(){var a=CKEDITOR.tools.isEmpty(CKEDITOR.instances),c;for(c in f){var d=f[c];a?d.destroy():d.element.hide()}a&&(f={})})}(),CKEDITOR.plugins.add("menu", {requires:"floatpanel",beforeInit:function(a){for(var f=a.config.menu_groups.split(","),b=a._.menuGroups={},c=a._.menuItems={},d=0;db.group?1:a.orderb.order?1:0})}var f='\x3cspan class\x3d"cke_menuitem"\x3e\x3ca id\x3d"{id}" class\x3d"cke_menubutton cke_menubutton__{name} cke_menubutton_{state} {cls}" href\x3d"{href}" title\x3d"{title}" tabindex\x3d"-1" _cke_focus\x3d1 hidefocus\x3d"true" role\x3d"{role}" aria-label\x3d"{label}" aria-describedby\x3d"{id}_description" aria-haspopup\x3d"{hasPopup}" aria-disabled\x3d"{disabled}" {ariaChecked} draggable\x3d"false"';CKEDITOR.env.gecko&&CKEDITOR.env.mac&& (f+=' onkeypress\x3d"return false;"');CKEDITOR.env.gecko&&(f+=' onblur\x3d"this.style.cssText \x3d this.style.cssText;" ondragstart\x3d"return false;"');var f=f+(' onmouseover\x3d"CKEDITOR.tools.callFunction({hoverFn},{index});" onmouseout\x3d"CKEDITOR.tools.callFunction({moveOutFn},{index});" '+(CKEDITOR.env.ie?'onclick\x3d"return false;" onmouseup':"onclick")+'\x3d"CKEDITOR.tools.callFunction({clickFn},{index}); return false;"\x3e'),b=CKEDITOR.addTemplate("menuItem",f+'\x3cspan class\x3d"cke_menubutton_inner"\x3e\x3cspan class\x3d"cke_menubutton_icon"\x3e\x3cspan class\x3d"cke_button_icon cke_button__{iconName}_icon" style\x3d"{iconStyle}"\x3e\x3c/span\x3e\x3c/span\x3e\x3cspan class\x3d"cke_menubutton_label"\x3e{label}\x3c/span\x3e{shortcutHtml}{arrowHtml}\x3c/span\x3e\x3c/a\x3e\x3cspan id\x3d"{id}_description" class\x3d"cke_voice_label" aria-hidden\x3d"false"\x3e{ariaShortcut}\x3c/span\x3e\x3c/span\x3e'), c=CKEDITOR.addTemplate("menuArrow",'\x3cspan class\x3d"cke_menuarrow"\x3e\x3cspan\x3e{label}\x3c/span\x3e\x3c/span\x3e'),d=CKEDITOR.addTemplate("menuShortcut",'\x3cspan class\x3d"cke_menubutton_label cke_menubutton_shortcut"\x3e{shortcut}\x3c/span\x3e');CKEDITOR.menu=CKEDITOR.tools.createClass({$:function(a,b){b=this._.definition=b||{};this.id=CKEDITOR.tools.getNextId();this.editor=a;this.items=[];this._.listeners=[];this._.level=b.level||1;var c=CKEDITOR.tools.extend({},b.panel,{css:[CKEDITOR.skin.getPath("editor")], level:this._.level-1,block:{}}),d=c.block.attributes=c.attributes||{};!d.role&&(d.role="menu");this._.panelDefinition=c},_:{onShow:function(){var a=this.editor.getSelection(),b=a&&a.getStartElement(),c=this.editor.elementPath(),d=this._.listeners;this.removeAll();for(var f=0;fz.length)return!1;h=f.getParents(!0);for(r=0;rx;r++)t[r].indent+=h;h=CKEDITOR.plugins.list.arrayToList(t,e,null,a.config.enterMode,f.getDirection());if(!d.isIndent){var C;if((C=f.getParent())&&C.is("li"))for(var z=h.listNode.getChildren(),u=[],p,r=z.count()-1;0<=r;r--)(p=z.getItem(r))&&p.is&&p.is("li")&&u.push(p)}h&&h.listNode.replace(f);if(u&&u.length)for(r=0;rg.length)){d=g[g.length-1].getNext();f=e.createElement(this.type);for(c.push(f);g.length;)c=g.shift(),a=e.createElement("li"),l=c,l.is("pre")||u.test(l.getName())||"false"==l.getAttribute("contenteditable")?c.appendTo(a):(c.copyAttributes(a),h&&c.getDirection()&&(a.removeStyle("direction"),a.removeAttribute("dir")),c.moveChildren(a),c.remove()),a.appendTo(f);h&&k&&f.setAttribute("dir",h);d?f.insertBefore(d):f.appendTo(b)}}function b(a,b,c){function d(c){if(!(!(l= k[c?"getFirst":"getLast"]())||l.is&&l.isBlockBoundary()||!(m=b.root[c?"getPrevious":"getNext"](CKEDITOR.dom.walker.invisible(!0)))||m.is&&m.isBlockBoundary({br:1})))a.document.createElement("br")[c?"insertBefore":"insertAfter"](l)}for(var e=CKEDITOR.plugins.list.listToArray(b.root,c),g=[],f=0;fe[f-1].indent+1){g=e[f-1].indent+1-e[f].indent;for(h=e[f].indent;e[f]&&e[f].indent>=h;)e[f].indent+=g,f++;f--}var k=CKEDITOR.plugins.list.arrayToList(e,c,null,a.config.enterMode,b.root.getAttribute("dir")).listNode,l,m;d(!0);d();k.replace(b.root);a.fire("contentDomInvalidated")}function c(a,b){this.name=a;this.context=this.type=b;this.allowedContent=b+" li";this.requiredContent=b}function d(a,b,c,d){for(var e, g;e=a[d?"getLast":"getFirst"](p);)(g=e.getDirection(1))!==b.getDirection(1)&&e.setAttribute("dir",g),e.remove(),c?e[d?"insertBefore":"insertAfter"](c):b.append(e,d),c=e}function l(a){function b(c){var e=a[c?"getPrevious":"getNext"](q);e&&e.type==CKEDITOR.NODE_ELEMENT&&e.is(a.getName())&&(d(a,e,null,!c),a.remove(),a=e)}b();b(1)}function k(a){return a.type==CKEDITOR.NODE_ELEMENT&&(a.getName()in CKEDITOR.dtd.$block||a.getName()in CKEDITOR.dtd.$listItem)&&CKEDITOR.dtd[a.getName()]["#"]}function g(a,b, c){a.fire("saveSnapshot");c.enlarge(CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS);var e=c.extractContents();b.trim(!1,!0);var g=b.createBookmark(),f=new CKEDITOR.dom.elementPath(b.startContainer),k=f.block,f=f.lastElement.getAscendant("li",1)||k,m=new CKEDITOR.dom.elementPath(c.startContainer),n=m.contains(CKEDITOR.dtd.$listItem),m=m.contains(CKEDITOR.dtd.$list);k?(k=k.getBogus())&&k.remove():m&&(k=m.getPrevious(q))&&y(k)&&k.remove();(k=e.getLast())&&k.type==CKEDITOR.NODE_ELEMENT&&k.is("br")&&k.remove();(k= b.startContainer.getChild(b.startOffset))?e.insertBefore(k):b.startContainer.append(e);n&&(e=h(n))&&(f.contains(n)?(d(e,n.getParent(),n),e.remove()):f.append(e));for(;c.checkStartOfBlock()&&c.checkEndOfBlock();){m=c.startPath();e=m.block;if(!e)break;e.is("li")&&(f=e.getParent(),e.equals(f.getLast(q))&&e.equals(f.getFirst(q))&&(e=f));c.moveToPosition(e,CKEDITOR.POSITION_BEFORE_START);e.remove()}c=c.clone();e=a.editable();c.setEndAt(e,CKEDITOR.POSITION_BEFORE_END);c=new CKEDITOR.dom.walker(c);c.evaluator= function(a){return q(a)&&!y(a)};(c=c.next())&&c.type==CKEDITOR.NODE_ELEMENT&&c.getName()in CKEDITOR.dtd.$list&&l(c);b.moveToBookmark(g);b.select();a.fire("saveSnapshot")}function h(a){return(a=a.getLast(q))&&a.type==CKEDITOR.NODE_ELEMENT&&a.getName()in m?a:null}var m={ol:1,ul:1},e=CKEDITOR.dom.walker.whitespaces(),n=CKEDITOR.dom.walker.bookmark(),q=function(a){return!(e(a)||n(a))},y=CKEDITOR.dom.walker.bogus();CKEDITOR.plugins.list={listToArray:function(a,b,c,d,e){if(!m[a.getName()])return[];d||(d= 0);c||(c=[]);for(var g=0,f=a.getChildCount();g=f.$.documentMode&&p.append(f.createText(" ")),p.append(l.listNode),l=l.nextIndex;else if(-1== G.indent&&!c&&g){m[g.getName()]?(p=G.element.clone(!1,!0),y!=g.getDirection(1)&&p.setAttribute("dir",y)):p=new CKEDITOR.dom.documentFragment(f);var k=g.getDirection(1)!=y,D=G.element,N=D.getAttribute("class"),Q=D.getAttribute("style"),O=p.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT&&(d!=CKEDITOR.ENTER_BR||k||Q||N),K,W=G.contents.length,R;for(g=0;gCKEDITOR.env.version? k.createText("\r"):k.createElement("br"),c.deleteContents(),c.insertNode(a),CKEDITOR.env.needsBrFiller?(k.createText("").insertAfter(a),l&&(v||p.blockLimit).appendBogus(),a.getNext().$.nodeValue="",c.setStartAt(a.getNext(),CKEDITOR.POSITION_AFTER_START)):c.setStartAt(a,CKEDITOR.POSITION_AFTER_END)),c.collapse(!0),c.select(),c.scrollIntoView()):g(a,b,c,d)}}};l=CKEDITOR.plugins.enterkey;k=l.enterBr;g=l.enterBlock;h=/^h[1-6]$/}(),function(){function a(a,b){var c={},d=[],l={nbsp:" ",shy:"­",gt:"\x3e", lt:"\x3c",amp:"\x26",apos:"'",quot:'"'};a=a.replace(/\b(nbsp|shy|gt|lt|amp|apos|quot)(?:,|$)/g,function(a,e){var g=b?"\x26"+e+";":l[e];c[g]=b?l[e]:"\x26"+e+";";d.push(g);return""});a=a.replace(/,$/,"");if(!b&&a){a=a.split(",");var k=document.createElement("div"),g;k.innerHTML="\x26"+a.join(";\x26")+";";g=k.innerHTML;k=null;for(k=0;kf&&(f=640);420>b&&(b=420);var d=parseInt((window.screen.height-b)/2,10),l=parseInt((window.screen.width- f)/2,10);c=(c||"location\x3dno,menubar\x3dno,toolbar\x3dno,dependent\x3dyes,minimizable\x3dno,modal\x3dyes,alwaysRaised\x3dyes,resizable\x3dyes,scrollbars\x3dyes")+",width\x3d"+f+",height\x3d"+b+",top\x3d"+d+",left\x3d"+l;var k=window.open("",null,c,!0);if(!k)return!1;try{-1==navigator.userAgent.toLowerCase().indexOf(" chrome/")&&(k.moveTo(l,d),k.resizeTo(f,b)),k.focus(),k.location.href=a}catch(g){window.open(a,null,c,!0)}return!0}}),"use strict",function(){function a(a){this.editor=a;this.loaders= []}function f(a,c,f){var g=a.config.fileTools_defaultFileName;this.editor=a;this.lang=a.lang;"string"===typeof c?(this.data=c,this.file=b(this.data),this.loaded=this.total=this.file.size):(this.data=null,this.file=c,this.total=this.file.size,this.loaded=0);f?this.fileName=f:this.file.name?this.fileName=this.file.name:(a=this.file.type.split("/"),g&&(a[0]=g),this.fileName=a.join("."));this.uploaded=0;this.responseData=this.uploadTotal=null;this.status="created";this.abort=function(){this.changeStatus("abort")}} function b(a){var b=a.match(c)[1];a=a.replace(c,"");a=atob(a);var f=[],g,h,m,e;for(g=0;gg.status||299w.height-v.bottom?g("pin"):g("bottom"),e=w.width/2,e=d.floatSpacePreferRight?"right":0p.width?"rtl"==d.contentsLangDirection?"right":"left":e-v.left>v.right-e?"left":"right",p.width>w.width?(e="left",n=0):(n="left"==e?0w.width&&(e="left"==e?"right":"left",n=0)),h.setStyle(e,b(("pin"==l?A:t)+n+("pin"==l?0:"left"==e?z:-z)))):(l="pin",g("pin"),k(e))}}}();if(l){var g=new CKEDITOR.template('\x3cdiv id\x3d"cke_{name}" class\x3d"cke {id} cke_reset_all cke_chrome cke_editor_{name} cke_float cke_{langDir} '+ CKEDITOR.env.cssClass+'" dir\x3d"{langDir}" title\x3d"'+(CKEDITOR.env.gecko?" ":"")+'" lang\x3d"{langCode}" role\x3d"application" style\x3d"{style}"'+(a.title?' aria-labelledby\x3d"cke_{name}_arialbl"':" ")+"\x3e"+(a.title?'\x3cspan id\x3d"cke_{name}_arialbl" class\x3d"cke_voice_label"\x3e{voiceLabel}\x3c/span\x3e':" ")+'\x3cdiv class\x3d"cke_inner"\x3e\x3cdiv id\x3d"{topId}" class\x3d"cke_top" role\x3d"presentation"\x3e{content}\x3c/div\x3e\x3c/div\x3e\x3c/div\x3e'),h=CKEDITOR.document.getBody().append(CKEDITOR.dom.element.createFromHtml(g.output({content:l, id:a.id,langDir:a.lang.dir,langCode:a.langCode,name:a.name,style:"display:none;z-index:"+(d.baseFloatZIndex-1),topId:a.ui.spaceId("top"),voiceLabel:a.title}))),m=CKEDITOR.tools.eventsBuffer(500,k),e=CKEDITOR.tools.eventsBuffer(100,k);h.unselectable();h.on("mousedown",function(a){a=a.data;a.getTarget().hasAscendant("a",1)||a.preventDefault()});a.on("focus",function(b){k(b);a.on("change",m.input);f.on("scroll",e.input);f.on("resize",e.input)});a.on("blur",function(){h.hide();a.removeListener("change", m.input);f.removeListener("scroll",e.input);f.removeListener("resize",e.input)});a.on("destroy",function(){f.removeListener("scroll",e.input);f.removeListener("resize",e.input);h.clearCustomData();h.remove()});a.focusManager.hasFocus&&h.show();a.focusManager.add(h,1)}}var f=CKEDITOR.document.getWindow(),b=CKEDITOR.tools.cssLength;CKEDITOR.plugins.add("floatingspace",{init:function(b){b.on("loaded",function(){a(this)},null,null,20)}})}(),CKEDITOR.plugins.add("listblock",{requires:"panel",onLoad:function(){var a= CKEDITOR.addTemplate("panel-list",'\x3cul role\x3d"presentation" class\x3d"cke_panel_list"\x3e{items}\x3c/ul\x3e'),f=CKEDITOR.addTemplate("panel-list-item",'\x3cli id\x3d"{id}" class\x3d"cke_panel_listItem" role\x3dpresentation\x3e\x3ca id\x3d"{id}_option" _cke_focus\x3d1 hidefocus\x3dtrue title\x3d"{title}" draggable\x3d"false" ondragstart\x3d"return false;" href\x3d"javascript:void(\'{val}\')" {onclick}\x3d"CKEDITOR.tools.callFunction({clickFn},\'{val}\'); return false;" role\x3d"option"\x3e{text}\x3c/a\x3e\x3c/li\x3e'), b=CKEDITOR.addTemplate("panel-list-group",'\x3ch1 id\x3d"{id}" draggable\x3d"false" ondragstart\x3d"return false;" class\x3d"cke_panel_grouptitle" role\x3d"presentation" \x3e{label}\x3c/h1\x3e'),c=/\'/g;CKEDITOR.ui.panel.prototype.addListBlock=function(a,b){return this.addBlock(a,new CKEDITOR.ui.listBlock(this.getHolderElement(),b))};CKEDITOR.ui.listBlock=CKEDITOR.tools.createClass({base:CKEDITOR.ui.panel.block,$:function(a,b){b=b||{};var c=b.attributes||(b.attributes={});(this.multiSelect=!!b.multiSelect)&& (c["aria-multiselectable"]=!0);!c.role&&(c.role="listbox");this.base.apply(this,arguments);this.element.setAttribute("role",c.role);c=this.keys;c[40]="next";c[9]="next";c[38]="prev";c[CKEDITOR.SHIFT+9]="prev";c[32]=CKEDITOR.env.ie?"mouseup":"click";CKEDITOR.env.ie&&(c[13]="mouseup");this._.pendingHtml=[];this._.pendingList=[];this._.items={};this._.groups={}},_:{close:function(){if(this._.started){var b=a.output({items:this._.pendingList.join("")});this._.pendingList=[];this._.pendingHtml.push(b); delete this._.started}},getClick:function(){this._.click||(this._.click=CKEDITOR.tools.addFunction(function(a){var b=this.toggle(a);if(this.onClick)this.onClick(a,b)},this));return this._.click}},proto:{add:function(a,b,k){var g=CKEDITOR.tools.getNextId();this._.started||(this._.started=1,this._.size=this._.size||0);this._.items[a]=g;var h;h=CKEDITOR.tools.htmlEncodeAttr(a).replace(c,"\\'");a={id:g,val:h,onclick:CKEDITOR.env.ie?'onclick\x3d"return false;" onmouseup':"onclick",clickFn:this._.getClick(), title:CKEDITOR.tools.htmlEncodeAttr(k||a),text:b||a};this._.pendingList.push(f.output(a))},startGroup:function(a){this._.close();var c=CKEDITOR.tools.getNextId();this._.groups[a]=c;this._.pendingHtml.push(b.output({id:c,label:a}))},commit:function(){this._.close();this.element.appendHtml(this._.pendingHtml.join(""));delete this._.size;this._.pendingHtml=[]},toggle:function(a){var b=this.isMarked(a);b?this.unmark(a):this.mark(a);return!b},hideGroup:function(a){var b=(a=this.element.getDocument().getById(this._.groups[a]))&& a.getNext();a&&(a.setStyle("display","none"),b&&"ul"==b.getName()&&b.setStyle("display","none"))},hideItem:function(a){this.element.getDocument().getById(this._.items[a]).setStyle("display","none")},showAll:function(){var a=this._.items,b=this._.groups,c=this.element.getDocument(),g;for(g in a)c.getById(a[g]).setStyle("display","");for(var f in b)a=c.getById(b[f]),g=a.getNext(),a.setStyle("display",""),g&&"ul"==g.getName()&&g.setStyle("display","")},mark:function(a){this.multiSelect||this.unmarkAll(); a=this._.items[a];var b=this.element.getDocument().getById(a);b.addClass("cke_selected");this.element.getDocument().getById(a+"_option").setAttribute("aria-selected",!0);this.onMark&&this.onMark(b)},markFirstDisplayed:function(){var a=this;this._.markFirstDisplayed(function(){a.multiSelect||a.unmarkAll()})},unmark:function(a){var b=this.element.getDocument();a=this._.items[a];var c=b.getById(a);c.removeClass("cke_selected");b.getById(a+"_option").removeAttribute("aria-selected");this.onUnmark&&this.onUnmark(c)}, unmarkAll:function(){var a=this._.items,b=this.element.getDocument(),c;for(c in a){var g=a[c];b.getById(g).removeClass("cke_selected");b.getById(g+"_option").removeAttribute("aria-selected")}this.onUnmark&&this.onUnmark()},isMarked:function(a){return this.element.getDocument().getById(this._.items[a]).hasClass("cke_selected")},focus:function(a){this._.focusIndex=-1;var b=this.element.getElementsByTag("a"),c,g=-1;if(a)for(c=this.element.getDocument().getById(this._.items[a]).getFirst();a=b.getItem(++g);){if(a.equals(c)){this._.focusIndex= g;break}}else this.element.focus();c&&setTimeout(function(){c.focus()},0)}}})}}),CKEDITOR.plugins.add("richcombo",{requires:"floatpanel,listblock,button",beforeInit:function(a){a.ui.addHandler(CKEDITOR.UI_RICHCOMBO,CKEDITOR.ui.richCombo.handler)}}),function(){var a='\x3cspan id\x3d"{id}" class\x3d"cke_combo cke_combo__{name} {cls}" role\x3d"presentation"\x3e\x3cspan id\x3d"{id}_label" class\x3d"cke_combo_label"\x3e{label}\x3c/span\x3e\x3ca class\x3d"cke_combo_button" title\x3d"{title}" tabindex\x3d"-1"'+ (CKEDITOR.env.gecko&&!CKEDITOR.env.hc?"":" href\x3d\"javascript:void('{titleJs}')\"")+' hidefocus\x3d"true" role\x3d"button" aria-labelledby\x3d"{id}_label" aria-haspopup\x3d"listbox"';CKEDITOR.env.gecko&&CKEDITOR.env.mac&&(a+=' onkeypress\x3d"return false;"');CKEDITOR.env.gecko&&(a+=' onblur\x3d"this.style.cssText \x3d this.style.cssText;"');var a=a+(' onkeydown\x3d"return CKEDITOR.tools.callFunction({keydownFn},event,this);" onfocus\x3d"return CKEDITOR.tools.callFunction({focusFn},event);" '+(CKEDITOR.env.ie? 'onclick\x3d"return false;" onmouseup':"onclick")+'\x3d"CKEDITOR.tools.callFunction({clickFn},this);return false;"\x3e\x3cspan id\x3d"{id}_text" class\x3d"cke_combo_text cke_combo_inlinelabel"\x3e{label}\x3c/span\x3e\x3cspan class\x3d"cke_combo_open"\x3e\x3cspan class\x3d"cke_combo_arrow"\x3e'+(CKEDITOR.env.hc?"\x26#9660;":CKEDITOR.env.air?"\x26nbsp;":"")+"\x3c/span\x3e\x3c/span\x3e\x3c/a\x3e\x3c/span\x3e"),f=CKEDITOR.addTemplate("combo",a);CKEDITOR.UI_RICHCOMBO="richcombo";CKEDITOR.ui.richCombo= CKEDITOR.tools.createClass({$:function(a){CKEDITOR.tools.extend(this,a,{canGroup:!1,title:a.label,modes:{wysiwyg:1},editorFocus:1});a=this.panel||{};delete this.panel;this.id=CKEDITOR.tools.getNextNumber();this.document=a.parent&&a.parent.getDocument()||CKEDITOR.document;a.className="cke_combopanel";a.block={multiSelect:a.multiSelect,attributes:a.attributes};a.toolbarRelated=!0;this._={panelDefinition:a,items:{},listeners:[]}},proto:{renderHtml:function(a){var c=[];this.render(a,c);return c.join("")}, render:function(a,c){function d(){if(this.getState()!=CKEDITOR.TRISTATE_ON){var c=this.modes[a.mode]?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED;a.readOnly&&!this.readOnly&&(c=CKEDITOR.TRISTATE_DISABLED);this.setState(c);this.setValue("");c!=CKEDITOR.TRISTATE_DISABLED&&this.refresh&&this.refresh()}}var l=CKEDITOR.env,k="cke_"+this.id,g=CKEDITOR.tools.addFunction(function(c){q&&(a.unlockSelection(1),q=0);m.execute(c)},this),h=this,m={id:k,combo:this,focus:function(){CKEDITOR.document.getById(k).getChild(1).focus()}, execute:function(c){var d=h._;if(d.state!=CKEDITOR.TRISTATE_DISABLED)if(h.createPanel(a),d.on)d.panel.hide();else{h.commit();var e=h.getValue();e?d.list.mark(e):d.list.unmarkAll();d.panel.showBlock(h.id,new CKEDITOR.dom.element(c),4)}},clickFn:g};this._.listeners.push(a.on("activeFilterChange",d,this));this._.listeners.push(a.on("mode",d,this));this._.listeners.push(a.on("selectionChange",d,this));!this.readOnly&&this._.listeners.push(a.on("readOnly",d,this));var e=CKEDITOR.tools.addFunction(function(a, b){a=new CKEDITOR.dom.event(a);var c=a.getKeystroke();switch(c){case 13:case 32:case 40:CKEDITOR.tools.callFunction(g,b);break;default:m.onkey(m,c)}a.preventDefault()}),n=CKEDITOR.tools.addFunction(function(){m.onfocus&&m.onfocus()}),q=0;m.keyDownFn=e;l={id:k,name:this.name||this.command,label:this.label,title:this.title,cls:this.className||"",titleJs:l.gecko&&!l.hc?"":(this.title||"").replace("'",""),keydownFn:e,focusFn:n,clickFn:g};f.output(l,c);if(this.onRender)this.onRender();return m},createPanel:function(a){if(!this._.panel){var c= this._.panelDefinition,d=this._.panelDefinition.block,f=c.parent||CKEDITOR.document.getBody(),k="cke_combopanel__"+this.name,g=new CKEDITOR.ui.floatPanel(a,f,c),c=g.addListBlock(this.id,d),h=this;g.onShow=function(){this.element.addClass(k);h.setState(CKEDITOR.TRISTATE_ON);h._.on=1;h.editorFocus&&!a.focusManager.hasFocus&&a.focus();if(h.onOpen)h.onOpen()};g.onHide=function(c){this.element.removeClass(k);h.setState(h.modes&&h.modes[a.mode]?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED);h._.on=0; if(!c&&h.onClose)h.onClose()};g.onEscape=function(){g.hide(1)};c.onClick=function(a,b){h.onClick&&h.onClick.call(h,a,b);g.hide()};this._.panel=g;this._.list=c;g.getBlock(this.id).onHide=function(){h._.on=0;h.setState(CKEDITOR.TRISTATE_OFF)};this.init&&this.init()}},setValue:function(a,c){this._.value=a;var d=this.document.getById("cke_"+this.id+"_text");d&&(a||c?d.removeClass("cke_combo_inlinelabel"):(c=this.label,d.addClass("cke_combo_inlinelabel")),d.setText("undefined"!=typeof c?c:a))},getValue:function(){return this._.value|| ""},unmarkAll:function(){this._.list.unmarkAll()},mark:function(a){this._.list.mark(a)},hideItem:function(a){this._.list.hideItem(a)},hideGroup:function(a){this._.list.hideGroup(a)},showAll:function(){this._.list.showAll()},add:function(a,c,d){this._.items[a]=d||a;this._.list.add(a,c,d)},startGroup:function(a){this._.list.startGroup(a)},commit:function(){this._.committed||(this._.list.commit(),this._.committed=1,CKEDITOR.ui.fire("ready",this));this._.committed=1},setState:function(a){if(this._.state!= a){var c=this.document.getById("cke_"+this.id);c.setState(a,"cke_combo");a==CKEDITOR.TRISTATE_DISABLED?c.setAttribute("aria-disabled",!0):c.removeAttribute("aria-disabled");this._.state=a}},getState:function(){return this._.state},enable:function(){this._.state==CKEDITOR.TRISTATE_DISABLED&&this.setState(this._.lastState)},disable:function(){this._.state!=CKEDITOR.TRISTATE_DISABLED&&(this._.lastState=this._.state,this.setState(CKEDITOR.TRISTATE_DISABLED))},destroy:function(){CKEDITOR.tools.array.forEach(this._.listeners, function(a){a.removeListener()});this._.listeners=[]}},statics:{handler:{create:function(a){return new CKEDITOR.ui.richCombo(a)}}}});CKEDITOR.ui.prototype.addRichCombo=function(a,c){this.add(a,CKEDITOR.UI_RICHCOMBO,c)}}(),CKEDITOR.plugins.add("format",{requires:"richcombo",init:function(a){if(!a.blockless){for(var f=a.config,b=a.lang.format,c=f.format_tags.split(";"),d={},l=0,k=[],g=0;gb&&aI.version?" ":R,g=a.hotNode&&a.hotNode.getText()==d&&a.element.equals(a.hotNode)&&a.lastCmdDirection===!!c;h(a,function(d){g&&a.hotNode&&a.hotNode.remove();d[c?"insertAfter":"insertBefore"](b);d.setAttributes({"data-cke-magicline-hot":1, "data-cke-magicline-dir":!!c});a.lastCmdDirection=!!c});I.ie||a.enterMode==CKEDITOR.ENTER_BR||a.hotNode.scrollIntoView();a.line.detach()}return function(g){g=g.getSelection().getStartElement();var f;g=g.getAscendant(U,1);if(!p(a,g)&&g&&!g.equals(a.editable)&&!g.contains(a.editable)){(f=k(g))&&"false"==f.getAttribute("contenteditable")&&(g=f);a.element=g;f=d(a,g,!c);var h;n(f)&&f.is(a.triggers)&&f.is(X)&&(!d(a,f,!c)||(h=d(a,f,!c))&&n(h)&&h.is(a.triggers))?e(f):(h=b(a,g),n(h)&&(d(a,h,!c)?(g=d(a,h,!c))&& n(g)&&g.is(a.triggers)&&e(h):e(h)))}}}()}}function e(a,b){if(!b||b.type!=CKEDITOR.NODE_ELEMENT||!b.$)return!1;var c=a.line;return c.wrap.equals(b)||c.wrap.contains(b)}function n(a){return a&&a.type==CKEDITOR.NODE_ELEMENT&&a.$}function q(a){if(!n(a))return!1;var b;(b=y(a))||(n(a)?(b={left:1,right:1,center:1},b=!(!b[a.getComputedStyle("float")]&&!b[a.getAttribute("align")])):b=!1);return b}function y(a){return!!{absolute:1,fixed:1}[a.getComputedStyle("position")]}function u(a,b){return n(b)?b.is(a.triggers): null}function p(a,b){if(!b)return!1;for(var c=b.getParents(1),d=c.length;d--;)for(var e=a.tabuList.length;e--;)if(c[d].hasAttribute(a.tabuList[e]))return!0;return!1}function v(a,b,c){b=b[c?"getLast":"getFirst"](function(b){return a.isRelevant(b)&&!b.is(ha)});if(!b)return!1;t(a,b);return c?b.size.top>a.mouse.y:b.size.bottom(a.inInlineMode?d.editable.top+d.editable.height/2:Math.min(d.editable.height,d.pane.height)/ 2),b=b[h?"getLast":"getFirst"](function(a){return!(P(a)||S(a))});if(!b)return null;e(a,b)&&(b=a.line.wrap[h?"getPrevious":"getNext"](function(a){return!(P(a)||S(a))}));if(!n(b)||q(b)||!u(a,b))return null;t(a,b);return!h&&0<=b.size.top&&l(c.y,0,b.size.top+g)?(a=a.inInlineMode||0===d.scroll.y?O:W,new f([null,b,G,Q,a])):h&&b.size.bottom<=d.pane.height&&l(c.y,b.size.bottom-g,d.pane.height)?(a=a.inInlineMode||l(b.size.bottom,d.pane.height-g,d.pane.height)?K:W,new f([b,null,D,Q,a])):null}function r(a){var c= a.mouse,e=a.view,g=a.triggerOffset,h=b(a);if(!h)return null;t(a,h);var g=Math.min(g,0|h.size.outerHeight/2),k=[],m,r;if(l(c.y,h.size.top-1,h.size.top+g))r=!1;else if(l(c.y,h.size.bottom-g,h.size.bottom+1))r=!0;else return null;if(q(h)||v(a,h,r)||h.getParent().is(Z))return null;var M=d(a,h,!r);if(M){if(M&&M.type==CKEDITOR.NODE_TEXT)return null;if(n(M)){if(q(M)||!u(a,M)||M.getParent().is(Z))return null;k=[M,h][r?"reverse":"concat"]().concat([N,Q])}}else h.equals(a.editable[r?"getLast":"getFirst"](a.isRelevant))? (x(a),r&&l(c.y,h.size.bottom-g,e.pane.height)&&l(h.size.bottom,e.pane.height-g,e.pane.height)?m=K:l(c.y,0,h.size.top+g)&&(m=O)):m=W,k=[null,h][r?"reverse":"concat"]().concat([r?D:G,Q,m,h.equals(a.editable[r?"getLast":"getFirst"](a.isRelevant))?r?K:O:W]);return 0 in k?new f(k):null}function z(a,b,c,d){for(var e=b.getDocumentPosition(),g={},f={},h={},k={},l=Y.length;l--;)g[Y[l]]=parseInt(b.getComputedStyle.call(b,"border-"+Y[l]+"-width"),10)||0,h[Y[l]]=parseInt(b.getComputedStyle.call(b,"padding-"+ Y[l]),10)||0,f[Y[l]]=parseInt(b.getComputedStyle.call(b,"margin-"+Y[l]),10)||0;c&&!d||A(a,d);k.top=e.y-(c?0:a.view.scroll.y);k.left=e.x-(c?0:a.view.scroll.x);k.outerWidth=b.$.offsetWidth;k.outerHeight=b.$.offsetHeight;k.height=k.outerHeight-(h.top+h.bottom+g.top+g.bottom);k.width=k.outerWidth-(h.left+h.right+g.left+g.right);k.bottom=k.top+k.outerHeight;k.right=k.left+k.outerWidth;a.inInlineMode&&(k.scroll={top:b.$.scrollTop,left:b.$.scrollLeft});return C({border:g,padding:h,margin:f,ignoreScroll:c}, k,!0)}function t(a,b,c){if(!n(b))return b.size=null;if(!b.size)b.size={};else if(b.size.ignoreScroll==c&&b.size.date>new Date-ba)return null;return C(b.size,z(a,b,c),{date:+new Date},!0)}function x(a,b){a.view.editable=z(a,a.editable,b,!0)}function A(a,b){a.view||(a.view={});var c=a.view;if(!(!b&&c&&c.date>new Date-ba)){var d=a.win,c=d.getScrollPosition(),d=d.getViewPaneSize();C(a.view,{scroll:{x:c.x,y:c.y,width:a.doc.$.documentElement.scrollWidth-d.width,height:a.doc.$.documentElement.scrollHeight- d.height},pane:{width:d.width,height:d.height,bottom:d.height+c.y},date:+new Date},!0)}}function B(a,b,c,d){for(var e=d,g=d,h=0,k=!1,l=!1,m=a.view.pane.height,n=a.mouse;n.y+hd.left-e.x&&cd.top-e.y&&bCKEDITOR.env.version,E=CKEDITOR.dtd,L={},G=128,D=64,N=32,Q=16, O=4,K=2,W=1,R=" ",Z=E.$listItem,ha=E.$tableContent,X=C({},E.$nonEditable,E.$empty),U=E.$block,ba=100,ca="width:0px;height:0px;padding:0px;margin:0px;display:block;z-index:9999;color:#fff;position:absolute;font-size: 0px;line-height:0px;",V=ca+"border-color:transparent;display:block;border-style:solid;",M="\x3cspan\x3e"+R+"\x3c/span\x3e";L[CKEDITOR.ENTER_BR]="br";L[CKEDITOR.ENTER_P]="p";L[CKEDITOR.ENTER_DIV]="div";f.prototype={set:function(a,b,c){this.properties=a+b+(c||W);return this},is:function(a){return(this.properties& a)==a}};var aa=function(){function a(b,c){var d=b.$.elementFromPoint(c.x,c.y);return d&&d.nodeType?new CKEDITOR.dom.element(d):null}return function(b,c,d){if(!b.mouse)return null;var g=b.doc,f=b.line.wrap;d=d||b.mouse;var h=a(g,d);c&&e(b,h)&&(f.hide(),h=a(g,d),f.show());return!h||h.type!=CKEDITOR.NODE_ELEMENT||!h.$||I.ie&&9>I.version&&!b.boundary.equals(h)&&!b.boundary.contains(h)?null:h}}(),P=CKEDITOR.dom.walker.whitespaces(),S=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_COMMENT),T=function(){function b(e){var g= e.element,f,h,k;if(!n(g)||g.contains(e.editable)||g.isReadOnly())return null;k=B(e,function(a,b){return!b.equals(a)},function(a,b){return aa(a,!0,b)},g);f=k.upper;h=k.lower;if(a(e,f,h))return k.set(N,8);if(f&&g.contains(f))for(;!f.getParent().equals(g);)f=f.getParent();else f=g.getFirst(function(a){return d(e,a)});if(h&&g.contains(h))for(;!h.getParent().equals(g);)h=h.getParent();else h=g.getLast(function(a){return d(e,a)});if(!f||!h)return null;t(e,f);t(e,h);if(!l(e.mouse.y,f.size.top,h.size.bottom))return null; for(var g=Number.MAX_VALUE,m,r,M,q;h&&!h.equals(f)&&(r=f.getNext(e.isRelevant));)m=Math.abs(c(e,f,r)-e.mouse.y),m|<\/font>)/,l=/h.width&&(c.resize_minWidth=h.width);c.resize_minHeight> h.height&&(c.resize_minHeight=h.height);CKEDITOR.document.on("mousemove",f);CKEDITOR.document.on("mouseup",b);a.document&&(a.document.on("mousemove",f),a.document.on("mouseup",b));d.preventDefault&&d.preventDefault()});a.on("destroy",function(){CKEDITOR.tools.removeFunction(n)});a.on("uiSpace",function(b){if("bottom"==b.data.space){var c="";m&&!e&&(c=" cke_resizer_horizontal");!m&&e&&(c=" cke_resizer_vertical");var g='\x3cspan id\x3d"'+d+'" class\x3d"cke_resizer'+c+" cke_resizer_"+l+'" title\x3d"'+ CKEDITOR.tools.htmlEncode(a.lang.common.resize)+'" onmousedown\x3d"CKEDITOR.tools.callFunction('+n+', event)"\x3e'+("ltr"==l?"◢":"◣")+"\x3c/span\x3e";"ltr"==l&&"ltr"==c?b.data.html+=g:b.data.html=g+b.data.html}},a,null,100);a.on("maximize",function(b){a.ui.space("resizer")[b.data==CKEDITOR.TRISTATE_ON?"hide":"show"]()})}}}),CKEDITOR.plugins.add("menubutton",{requires:"button,menu",onLoad:function(){var a=function(a){var b=this._,c=b.menu;b.state!==CKEDITOR.TRISTATE_DISABLED&&(b.on&&c?c.hide():(b.previousState= b.state,c||(c=b.menu=new CKEDITOR.menu(a,{panel:{className:"cke_menu_panel",attributes:{"aria-label":a.lang.common.options}}}),c.onHide=CKEDITOR.tools.bind(function(){var c=this.command?a.getCommand(this.command).modes:this.modes;this.setState(!c||c[a.mode]?b.previousState:CKEDITOR.TRISTATE_DISABLED);b.on=0},this),this.onMenu&&c.addListener(this.onMenu)),this.setState(CKEDITOR.TRISTATE_ON),b.on=1,setTimeout(function(){c.show(CKEDITOR.document.getById(b.id),4)},0)))};CKEDITOR.ui.menuButton=CKEDITOR.tools.createClass({base:CKEDITOR.ui.button, $:function(f){delete f.panel;this.base(f);this.hasArrow="menu";this.click=a},statics:{handler:{create:function(a){return new CKEDITOR.ui.menuButton(a)}}}})},beforeInit:function(a){a.ui.addHandler(CKEDITOR.UI_MENUBUTTON,CKEDITOR.ui.menuButton.handler)}}),CKEDITOR.UI_MENUBUTTON="menubutton","use strict",CKEDITOR.plugins.add("scayt",{requires:"menubutton,dialog",tabToOpen:null,dialogName:"scaytDialog",onLoad:function(a){"moono-lisa"==(CKEDITOR.skinName||a.config.skin)&&CKEDITOR.document.appendStyleSheet(CKEDITOR.getUrl(this.path+ "skins/"+CKEDITOR.skin.name+"/scayt.css"));CKEDITOR.document.appendStyleSheet(CKEDITOR.getUrl(this.path+"dialogs/dialog.css"))},init:function(a){var f=this,b=CKEDITOR.plugins.scayt;this.bindEvents(a);this.parseConfig(a);this.addRule(a);CKEDITOR.dialog.add(this.dialogName,CKEDITOR.getUrl(this.path+"dialogs/options.js"));this.addMenuItems(a);var c=a.lang.scayt,d=CKEDITOR.env;a.ui.add("Scayt",CKEDITOR.UI_MENUBUTTON,{label:c.text_title,title:a.plugins.wsc?a.lang.wsc.title:c.text_title,modes:{wysiwyg:!(d.ie&& (8>d.version||d.quirks))},toolbar:"spellchecker,20",refresh:function(){var c=a.ui.instances.Scayt.getState();a.scayt&&(c=b.state.scayt[a.name]?CKEDITOR.TRISTATE_ON:CKEDITOR.TRISTATE_OFF);a.fire("scaytButtonState",c)},onRender:function(){var b=this;a.on("scaytButtonState",function(a){void 0!==typeof a.data&&b.setState(a.data)})},onMenu:function(){var c=a.scayt;a.getMenuItem("scaytToggle").label=a.lang.scayt[c&&b.state.scayt[a.name]?"btn_disable":"btn_enable"];var d={scaytToggle:CKEDITOR.TRISTATE_OFF, scaytOptions:c?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,scaytLangs:c?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,scaytDict:c?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,scaytAbout:c?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,WSC:a.plugins.wsc?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED};a.config.scayt_uiTabs[0]||delete d.scaytOptions;a.config.scayt_uiTabs[1]||delete d.scaytLangs;a.config.scayt_uiTabs[2]||delete d.scaytDict;c&&!CKEDITOR.plugins.scayt.isNewUdSupported(c)&& (delete d.scaytDict,a.config.scayt_uiTabs[2]=0,CKEDITOR.plugins.scayt.alarmCompatibilityMessage());return d}});a.contextMenu&&a.addMenuItems&&(a.contextMenu.addListener(function(b,c){var d=a.scayt,h,m;d&&(m=d.getSelectionNode())&&(h=f.menuGenerator(a,m),d.showBanner("."+a.contextMenu._.definition.panel.className.split(" ").join(" .")));return h}),a.contextMenu._.onHide=CKEDITOR.tools.override(a.contextMenu._.onHide,function(b){return function(){var c=a.scayt;c&&c.hideBanner();return b.apply(this)}}))}, addMenuItems:function(a){var f=this,b=CKEDITOR.plugins.scayt;a.addMenuGroup("scaytButton");for(var c=a.config.scayt_contextMenuItemsOrder.split("|"),d=0;da.config.scayt_maxSuggestions)a.config.scayt_maxSuggestions=3;if(void 0===a.config.scayt_minWordLength||"number"!=typeof a.config.scayt_minWordLength||1>a.config.scayt_minWordLength)a.config.scayt_minWordLength=3;if(void 0===a.config.scayt_customDictionaryIds||"string"!==typeof a.config.scayt_customDictionaryIds)a.config.scayt_customDictionaryIds="";if(void 0=== a.config.scayt_userDictionaryName||"string"!==typeof a.config.scayt_userDictionaryName)a.config.scayt_userDictionaryName=null;if("string"===typeof a.config.scayt_uiTabs&&3===a.config.scayt_uiTabs.split(",").length){var b=[],c=[];a.config.scayt_uiTabs=a.config.scayt_uiTabs.split(",");CKEDITOR.tools.search(a.config.scayt_uiTabs,function(a){1===Number(a)||0===Number(a)?(c.push(!0),b.push(Number(a))):c.push(!1)});null===CKEDITOR.tools.search(c,!1)?a.config.scayt_uiTabs=b:a.config.scayt_uiTabs=[1,1,1]}else a.config.scayt_uiTabs= [1,1,1];"string"!=typeof a.config.scayt_serviceProtocol&&(a.config.scayt_serviceProtocol=null);"string"!=typeof a.config.scayt_serviceHost&&(a.config.scayt_serviceHost=null);"string"!=typeof a.config.scayt_servicePort&&(a.config.scayt_servicePort=null);"string"!=typeof a.config.scayt_servicePath&&(a.config.scayt_servicePath=null);a.config.scayt_moreSuggestions||(a.config.scayt_moreSuggestions="on");"string"!==typeof a.config.scayt_customerId&&(a.config.scayt_customerId="1:WvF0D4-UtPqN1-43nkD4-NKvUm2-daQqk3-LmNiI-z7Ysb4-mwry24-T8YrS3-Q2tpq2"); "string"!==typeof a.config.scayt_customPunctuation&&(a.config.scayt_customPunctuation="-");"string"!==typeof a.config.scayt_srcUrl&&(f=document.location.protocol,f=-1!=f.search(/https?:/)?f:"http:",a.config.scayt_srcUrl=f+"//svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js");"boolean"!==typeof CKEDITOR.config.scayt_handleCheckDirty&&(CKEDITOR.config.scayt_handleCheckDirty=!0);"boolean"!==typeof CKEDITOR.config.scayt_handleUndoRedo&&(CKEDITOR.config.scayt_handleUndoRedo=!0);CKEDITOR.config.scayt_handleUndoRedo= CKEDITOR.plugins.undo?CKEDITOR.config.scayt_handleUndoRedo:!1;"boolean"!==typeof a.config.scayt_multiLanguageMode&&(a.config.scayt_multiLanguageMode=!1);"object"!==typeof a.config.scayt_multiLanguageStyles&&(a.config.scayt_multiLanguageStyles={});a.config.scayt_ignoreAllCapsWords&&"boolean"!==typeof a.config.scayt_ignoreAllCapsWords&&(a.config.scayt_ignoreAllCapsWords=!1);a.config.scayt_ignoreDomainNames&&"boolean"!==typeof a.config.scayt_ignoreDomainNames&&(a.config.scayt_ignoreDomainNames=!1);a.config.scayt_ignoreWordsWithMixedCases&& "boolean"!==typeof a.config.scayt_ignoreWordsWithMixedCases&&(a.config.scayt_ignoreWordsWithMixedCases=!1);a.config.scayt_ignoreWordsWithNumbers&&"boolean"!==typeof a.config.scayt_ignoreWordsWithNumbers&&(a.config.scayt_ignoreWordsWithNumbers=!1);if(a.config.scayt_disableOptionsStorage){var f=CKEDITOR.tools.isArray(a.config.scayt_disableOptionsStorage)?a.config.scayt_disableOptionsStorage:"string"===typeof a.config.scayt_disableOptionsStorage?[a.config.scayt_disableOptionsStorage]:void 0,d="all options lang ignore-all-caps-words ignore-domain-names ignore-words-with-mixed-cases ignore-words-with-numbers".split(" "), l=["lang","ignore-all-caps-words","ignore-domain-names","ignore-words-with-mixed-cases","ignore-words-with-numbers"],k=CKEDITOR.tools.search,g=CKEDITOR.tools.indexOf;a.config.scayt_disableOptionsStorage=function(a){for(var b=[],c=0;ca.config.scayt_cacheSize)a.config.scayt_cacheSize=4E3},addRule:function(a){var f=CKEDITOR.plugins.scayt,b=a.dataProcessor,c=b&&b.htmlFilter,d=a._.elementsPath&&a._.elementsPath.filters,b=b&&b.dataFilter,l=a.addRemoveFormatFilter,k=function(b){if(a.scayt&&(b.hasAttribute(f.options.data_attribute_name)||b.hasAttribute(f.options.problem_grammar_data_attribute)))return!1},g=function(b){var c= !0;a.scayt&&(b.hasAttribute(f.options.data_attribute_name)||b.hasAttribute(f.options.problem_grammar_data_attribute))&&(c=!1);return c};d&&d.push(k);b&&b.addRules({elements:{span:function(a){var b=a.hasClass(f.options.misspelled_word_class)&&a.attributes[f.options.data_attribute_name],c=a.hasClass(f.options.problem_grammar_class)&&a.attributes[f.options.problem_grammar_data_attribute];f&&(b||c)&&delete a.name;return a}}});c&&c.addRules({elements:{span:function(a){var b=a.hasClass(f.options.misspelled_word_class)&& a.attributes[f.options.data_attribute_name],c=a.hasClass(f.options.problem_grammar_class)&&a.attributes[f.options.problem_grammar_data_attribute];f&&(b||c)&&delete a.name;return a}}});l&&l.call(a,g)},scaytMenuDefinition:function(a){var f=this,b=CKEDITOR.plugins.scayt;a=a.scayt;return{scayt:{scayt_ignore:{label:a.getLocal("btn_ignore"),group:"scayt_control",order:1,exec:function(a){a.scayt.ignoreWord()}},scayt_ignoreall:{label:a.getLocal("btn_ignoreAll"),group:"scayt_control",order:2,exec:function(a){a.scayt.ignoreAllWords()}}, scayt_add:{label:a.getLocal("btn_addWord"),group:"scayt_control",order:3,exec:function(a){var b=a.scayt;setTimeout(function(){b.addWordToUserDictionary()},10)}},scayt_option:{label:a.getLocal("btn_options"),group:"scayt_control",order:4,exec:function(a){a.scayt.tabToOpen="options";b.openDialog(f.dialogName,a)},verification:function(a){return 1==a.config.scayt_uiTabs[0]?!0:!1}},scayt_language:{label:a.getLocal("btn_langs"),group:"scayt_control",order:5,exec:function(a){a.scayt.tabToOpen="langs";b.openDialog(f.dialogName, a)},verification:function(a){return 1==a.config.scayt_uiTabs[1]?!0:!1}},scayt_dictionary:{label:a.getLocal("btn_dictionaries"),group:"scayt_control",order:6,exec:function(a){a.scayt.tabToOpen="dictionaries";b.openDialog(f.dialogName,a)},verification:function(a){return 1==a.config.scayt_uiTabs[2]?!0:!1}},scayt_about:{label:a.getLocal("btn_about"),group:"scayt_control",order:7,exec:function(a){a.scayt.tabToOpen="about";b.openDialog(f.dialogName,a)}}},grayt:{grayt_problemdescription:{label:"Grammar problem description", group:"grayt_description",order:1,state:CKEDITOR.TRISTATE_DISABLED,exec:function(a){}},grayt_ignore:{label:a.getLocal("btn_ignore"),group:"grayt_control",order:2,exec:function(a){a.scayt.ignorePhrase()}},grayt_ignoreall:{label:a.getLocal("btn_ignoreAll"),group:"grayt_control",order:3,exec:function(a){a.scayt.ignoreAllPhrases()}}}}},buildSuggestionMenuItems:function(a,f,b){var c={},d={},l=b?"word":"phrase",k=b?"startGrammarCheck":"startSpellCheck",g=a.scayt;if(0=parseInt(f)*Math.pow(10,b)}return f?Array(7).join(String.fromCharCode(8203)):String.fromCharCode(8203)}()}],state:{scayt:{},grayt:{}},warningCounter:0,suggestions:[],options:{disablingCommandExec:{source:!0,newpage:!0,templates:!0},data_attribute_name:"data-scayt-word",misspelled_word_class:"scayt-misspell-word",problem_grammar_data_attribute:"data-grayt-phrase",problem_grammar_class:"gramm-problem"},backCompatibilityMap:{scayt_service_protocol:"scayt_serviceProtocol",scayt_service_host:"scayt_serviceHost", scayt_service_port:"scayt_servicePort",scayt_service_path:"scayt_servicePath",scayt_customerid:"scayt_customerId"},openDialog:function(a,f){var b=f.scayt;b.isAllModulesReady&&!1===b.isAllModulesReady()||(f.lockSelection(),f.openDialog(a))},alarmCompatibilityMessage:function(){5>this.warningCounter&&(console.warn("You are using the latest version of SCAYT plugin for CKEditor with the old application version. In order to have access to the newest features, it is recommended to upgrade the application version to latest one as well. Contact us for more details at support@webspellchecker.net."), this.warningCounter+=1)},isNewUdSupported:function(a){return a.getUserDictionary?!0:!1},reloadMarkup:function(a){var f;a&&(f=a.getScaytLangList(),a.reloadMarkup?a.reloadMarkup():(this.alarmCompatibilityMessage(),f&&f.ltr&&f.rtl&&a.fire("startSpellCheck, startGrammarCheck")))},replaceOldOptionsNames:function(a){for(var f in a)f in this.backCompatibilityMap&&(a[this.backCompatibilityMap[f]]=a[f],delete a[f])},createScayt:function(a){var f=this,b=CKEDITOR.plugins.scayt;this.loadScaytLibrary(a,function(a){function d(a){return new SCAYT.CKSCAYT(a, function(){},function(){})}var l;a.window&&(l="BODY"==a.editable().$.nodeName?a.window.getFrame():a.editable());if(l){l={lang:a.config.scayt_sLang,container:l.$,customDictionary:a.config.scayt_customDictionaryIds,userDictionaryName:a.config.scayt_userDictionaryName,localization:a.langCode,customer_id:a.config.scayt_customerId,customPunctuation:a.config.scayt_customPunctuation,debug:a.config.scayt_debug,data_attribute_name:f.options.data_attribute_name,misspelled_word_class:f.options.misspelled_word_class, problem_grammar_data_attribute:f.options.problem_grammar_data_attribute,problem_grammar_class:f.options.problem_grammar_class,"options-to-restore":a.config.scayt_disableOptionsStorage,focused:a.editable().hasFocus,ignoreElementsRegex:a.config.scayt_elementsToIgnore,ignoreGraytElementsRegex:a.config.grayt_elementsToIgnore,minWordLength:a.config.scayt_minWordLength,multiLanguageMode:a.config.scayt_multiLanguageMode,multiLanguageStyles:a.config.scayt_multiLanguageStyles,graytAutoStartup:a.config.grayt_autoStartup, disableCache:a.config.scayt_disableCache,cacheSize:a.config.scayt_cacheSize,charsToObserve:b.charsToObserve};a.config.scayt_serviceProtocol&&(l.service_protocol=a.config.scayt_serviceProtocol);a.config.scayt_serviceHost&&(l.service_host=a.config.scayt_serviceHost);a.config.scayt_servicePort&&(l.service_port=a.config.scayt_servicePort);a.config.scayt_servicePath&&(l.service_path=a.config.scayt_servicePath);"boolean"===typeof a.config.scayt_ignoreAllCapsWords&&(l["ignore-all-caps-words"]=a.config.scayt_ignoreAllCapsWords); "boolean"===typeof a.config.scayt_ignoreDomainNames&&(l["ignore-domain-names"]=a.config.scayt_ignoreDomainNames);"boolean"===typeof a.config.scayt_ignoreWordsWithMixedCases&&(l["ignore-words-with-mixed-cases"]=a.config.scayt_ignoreWordsWithMixedCases);"boolean"===typeof a.config.scayt_ignoreWordsWithNumbers&&(l["ignore-words-with-numbers"]=a.config.scayt_ignoreWordsWithNumbers);var k;try{k=d(l)}catch(g){f.alarmCompatibilityMessage(),delete l.charsToObserve,k=d(l)}k.subscribe("suggestionListSend", function(a){for(var b={},c=[],d=0;d=parseInt(a.getAttribute("border"),10))&&a.addClass("cke_show_border")})},afterInit:function(a){var b=a.dataProcessor;a=b&&b.dataFilter;b=b&&b.htmlFilter;a&&a.addRules({elements:{table:function(a){a=a.attributes;var b=a["class"],f=parseInt(a.border,10);f&&!(0>=f)||b&&-1!=b.indexOf("cke_show_border")||(a["class"]=(b||"")+" cke_show_border")}}});b&&b.addRules({elements:{table:function(a){a=a.attributes;var b=a["class"];b&&(a["class"]=b.replace("cke_show_border","").replace(/\s{2}/," ").replace(/^\s+|\s+$/, ""))}}})}});CKEDITOR.on("dialogDefinition",function(a){var b=a.data.name;if("table"==b||"tableProperties"==b)if(a=a.data.definition,b=a.getContents("info").get("txtBorder"),b.commit=CKEDITOR.tools.override(b.commit,function(a){return function(b,f){a.apply(this,arguments);var k=parseInt(this.getValue(),10);f[!k||0>=k?"addClass":"removeClass"]("cke_show_border")}}),a=(a=a.getContents("advanced"))&&a.get("advCSSClasses"))a.setup=CKEDITOR.tools.override(a.setup,function(a){return function(){a.apply(this, arguments);this.setValue(this.getValue().replace(/cke_show_border/,""))}}),a.commit=CKEDITOR.tools.override(a.commit,function(a){return function(b,f){a.apply(this,arguments);parseInt(f.getAttribute("border"),10)||f.addClass("cke_show_border")}})})}(),function(){CKEDITOR.plugins.add("sourcearea",{init:function(f){function b(){var a=d&&this.equals(CKEDITOR.document.getActive());this.hide();this.setStyle("height",this.getParent().$.clientHeight+"px");this.setStyle("width",this.getParent().$.clientWidth+ "px");this.show();a&&this.focus()}if(f.elementMode!=CKEDITOR.ELEMENT_MODE_INLINE){var c=CKEDITOR.plugins.sourcearea;f.addMode("source",function(c){var d=f.ui.space("contents").getDocument().createElement("textarea");d.setStyles(CKEDITOR.tools.extend({width:CKEDITOR.env.ie7Compat?"99%":"100%",height:"100%",resize:"none",outline:"none","text-align":"left"},CKEDITOR.tools.cssVendorPrefix("tab-size",f.config.sourceAreaTabSize||4)));d.setAttribute("dir","ltr");d.addClass("cke_source").addClass("cke_reset").addClass("cke_enable_context_menu"); f.ui.space("contents").append(d);d=f.editable(new a(f,d));d.setData(f.getData(1));CKEDITOR.env.ie&&(d.attachListener(f,"resize",b,d),d.attachListener(CKEDITOR.document.getWindow(),"resize",b,d),CKEDITOR.tools.setTimeout(b,0,d));f.fire("ariaWidget",this);c()});f.addCommand("source",c.commands.source);f.ui.addButton&&f.ui.addButton("Source",{label:f.lang.sourcearea.toolbar,command:"source",toolbar:"mode,10"});f.on("mode",function(){f.getCommand("source").setState("source"==f.mode?CKEDITOR.TRISTATE_ON: CKEDITOR.TRISTATE_OFF)});var d=CKEDITOR.env.ie&&9==CKEDITOR.env.version}}});var a=CKEDITOR.tools.createClass({base:CKEDITOR.editable,proto:{setData:function(a){this.setValue(a);this.status="ready";this.editor.fire("dataReady")},getData:function(){return this.getValue()},insertHtml:function(){},insertElement:function(){},insertText:function(){},setReadOnly:function(a){this[(a?"set":"remove")+"Attribute"]("readOnly","readonly")},detach:function(){a.baseProto.detach.call(this);this.clearCustomData(); this.remove()}}})}(),CKEDITOR.plugins.sourcearea={commands:{source:{modes:{wysiwyg:1,source:1},editorFocus:!1,readOnly:1,exec:function(a){"wysiwyg"==a.mode&&a.fire("saveSnapshot");a.getCommand("source").setState(CKEDITOR.TRISTATE_DISABLED);a.setMode("source"==a.mode?"wysiwyg":"source")},canUndo:!1}}},CKEDITOR.plugins.add("specialchar",{availableLangs:{af:1,ar:1,az:1,bg:1,ca:1,cs:1,cy:1,da:1,de:1,"de-ch":1,el:1,en:1,"en-au":1,"en-ca":1,"en-gb":1,eo:1,es:1,"es-mx":1,et:1,eu:1,fa:1,fi:1,fr:1,"fr-ca":1, gl:1,he:1,hr:1,hu:1,id:1,it:1,ja:1,km:1,ko:1,ku:1,lt:1,lv:1,nb:1,nl:1,no:1,oc:1,pl:1,pt:1,"pt-br":1,ro:1,ru:1,si:1,sk:1,sl:1,sq:1,sr:1,"sr-latn":1,sv:1,th:1,tr:1,tt:1,ug:1,uk:1,vi:1,zh:1,"zh-cn":1},requires:"dialog",init:function(a){var f=this;CKEDITOR.dialog.add("specialchar",this.path+"dialogs/specialchar.js");a.addCommand("specialchar",{exec:function(){var b=a.langCode,b=f.availableLangs[b]?b:f.availableLangs[b.replace(/-.*/,"")]?b.replace(/-.*/,""):"en";CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(f.path+ "dialogs/lang/"+b+".js"),function(){CKEDITOR.tools.extend(a.lang.specialchar,f.langEntries[b]);a.openDialog("specialchar")})},modes:{wysiwyg:1},canUndo:!1});a.ui.addButton&&a.ui.addButton("SpecialChar",{label:a.lang.specialchar.toolbar,command:"specialchar",toolbar:"insert,50"})}}),CKEDITOR.config.specialChars="! \x26quot; # $ % \x26amp; ' ( ) * + - . / 0 1 2 3 4 5 6 7 8 9 : ; \x26lt; \x3d \x26gt; ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ \x26euro; \x26lsquo; \x26rsquo; \x26ldquo; \x26rdquo; \x26ndash; \x26mdash; \x26iexcl; \x26cent; \x26pound; \x26curren; \x26yen; \x26brvbar; \x26sect; \x26uml; \x26copy; \x26ordf; \x26laquo; \x26not; \x26reg; \x26macr; \x26deg; \x26sup2; \x26sup3; \x26acute; \x26micro; \x26para; \x26middot; \x26cedil; \x26sup1; \x26ordm; \x26raquo; \x26frac14; \x26frac12; \x26frac34; \x26iquest; \x26Agrave; \x26Aacute; \x26Acirc; \x26Atilde; \x26Auml; \x26Aring; \x26AElig; \x26Ccedil; \x26Egrave; \x26Eacute; \x26Ecirc; \x26Euml; \x26Igrave; \x26Iacute; \x26Icirc; \x26Iuml; \x26ETH; \x26Ntilde; \x26Ograve; \x26Oacute; \x26Ocirc; \x26Otilde; \x26Ouml; \x26times; \x26Oslash; \x26Ugrave; \x26Uacute; \x26Ucirc; \x26Uuml; \x26Yacute; \x26THORN; \x26szlig; \x26agrave; \x26aacute; \x26acirc; \x26atilde; \x26auml; \x26aring; \x26aelig; \x26ccedil; \x26egrave; \x26eacute; \x26ecirc; \x26euml; \x26igrave; \x26iacute; \x26icirc; \x26iuml; \x26eth; \x26ntilde; \x26ograve; \x26oacute; \x26ocirc; \x26otilde; \x26ouml; \x26divide; \x26oslash; \x26ugrave; \x26uacute; \x26ucirc; \x26uuml; \x26yacute; \x26thorn; \x26yuml; \x26OElig; \x26oelig; \x26#372; \x26#374 \x26#373 \x26#375; \x26sbquo; \x26#8219; \x26bdquo; \x26hellip; \x26trade; \x26#9658; \x26bull; \x26rarr; \x26rArr; \x26hArr; \x26diams; \x26asymp;".split(" "), function(){CKEDITOR.plugins.add("stylescombo",{requires:"richcombo",init:function(a){var f=a.config,b=a.lang.stylescombo,c={},d=[],l=[];a.on("stylesSet",function(b){if(b=b.data.styles){for(var g,h,m,e=0,n=b.length;e=b)for(g=this.getNextSourceNode(a,CKEDITOR.NODE_ELEMENT);g;){if(g.isVisible()&&0===g.getTabIndex()){l=g;break}g=g.getNextSourceNode(!1,CKEDITOR.NODE_ELEMENT)}else for(g=this.getDocument().getBody().getFirst();g=g.getNextSourceNode(!1,CKEDITOR.NODE_ELEMENT);){if(!c)if(!d&&g.equals(this)){if(d=!0,a){if(!(g=g.getNextSourceNode(!0,CKEDITOR.NODE_ELEMENT)))break;c=1}}else d&&!this.contains(g)&&(c=1);if(g.isVisible()&&!(0>(h=g.getTabIndex()))){if(c&&h==b){l= g;break}h>b&&(!l||!k||h(g=h.getTabIndex())))if(0>=b){if(c&&0===g){l=h;break}g>k&& (l=h,k=g)}else{if(c&&g==b){l=h;break}gk)&&(l=h,k=g)}}l&&l.focus()},CKEDITOR.plugins.add("table",{requires:"dialog",init:function(a){function f(a){return CKEDITOR.tools.extend(a||{},{contextSensitive:1,refresh:function(a,b){this.setState(b.contains("table",1)?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED)}})}if(!a.blockless){var b=a.lang.table;a.addCommand("table",new CKEDITOR.dialogCommand("table",{context:"table",allowedContent:"table{width,height,border-collapse}[align,border,cellpadding,cellspacing,summary];caption tbody thead tfoot;th td tr[scope];td{border*,background-color,vertical-align,width,height}[colspan,rowspan];"+ (a.plugins.dialogadvtab?"table"+a.plugins.dialogadvtab.allowedContent():""),requiredContent:"table",contentTransformations:[["table{width}: sizeToStyle","table[width]: sizeToAttribute"],["td: splitBorderShorthand"],[{element:"table",right:function(a){if(a.styles){var b;if(a.styles.border)b=CKEDITOR.tools.style.parse.border(a.styles.border);else if(CKEDITOR.env.ie&&8===CKEDITOR.env.version){var f=a.styles;f["border-left"]&&f["border-left"]===f["border-right"]&&f["border-right"]===f["border-top"]&& f["border-top"]===f["border-bottom"]&&(b=CKEDITOR.tools.style.parse.border(f["border-top"]))}b&&b.style&&"solid"===b.style&&b.width&&0!==parseFloat(b.width)&&(a.attributes.border=1);"collapse"==a.styles["border-collapse"]&&(a.attributes.cellspacing=0)}}}]]}));a.addCommand("tableProperties",new CKEDITOR.dialogCommand("tableProperties",f()));a.addCommand("tableDelete",f({exec:function(a){var b=a.elementPath().contains("table",1);if(b){var f=b.getParent(),k=a.editable();1!=f.getChildCount()||f.is("td", "th")||f.equals(k)||(b=f);a=a.createRange();a.moveToPosition(b,CKEDITOR.POSITION_BEFORE_START);b.remove();a.select()}}}));a.ui.addButton&&a.ui.addButton("Table",{label:b.toolbar,command:"table",toolbar:"insert,30"});CKEDITOR.dialog.add("table",this.path+"dialogs/table.js");CKEDITOR.dialog.add("tableProperties",this.path+"dialogs/table.js");a.addMenuItems&&a.addMenuItems({table:{label:b.menu,command:"tableProperties",group:"table",order:5},tabledelete:{label:b.deleteTable,command:"tableDelete",group:"table", order:1}});a.on("doubleclick",function(a){a.data.element.is("table")&&(a.data.dialog="tableProperties")});a.contextMenu&&a.contextMenu.addListener(function(){return{tabledelete:CKEDITOR.TRISTATE_OFF,table:CKEDITOR.TRISTATE_OFF}})}}}),function(){function a(a,b){function c(a){return b?b.contains(a)&&a.getAscendant("table",!0).equals(b):!0}function d(a){0d)d=g}return d}function l(b,c){for(var e=p(b)?b:a(b),g=e[0].getAscendant("table"),f=d(e,1),e=d(e),h=c?f:e,k=CKEDITOR.tools.buildTableMap(g),g=[],f=[],e=[],l=k.length,m=0;ml?new CKEDITOR.dom.element(h[0][l+1]):k&&-1!==h[0][k-1].cellIndex?new CKEDITOR.dom.element(h[0][k-1]):new CKEDITOR.dom.element(e.$.parentNode);m.length==b&&(d[0].moveToPosition(e,CKEDITOR.POSITION_AFTER_END),d[0].select(),e.remove());return k}function g(a,b){var c=a.getStartElement().getAscendant({td:1,th:1},!0);if(c){var d=c.clone();d.appendBogus();b?d.insertBefore(c):d.insertAfter(c)}}function h(b){if(b instanceof CKEDITOR.dom.selection){var c= b.getRanges(),d=a(b),e=d[0]&&d[0].getAscendant("table"),g;a:{var f=0;g=d.length-1;for(var k={},l,n;l=d[f++];)CKEDITOR.dom.element.setMarker(k,l,"delete_cell",!0);for(f=0;l=d[f++];)if((n=l.getPrevious())&&!n.getCustomData("delete_cell")||(n=l.getNext())&&!n.getCustomData("delete_cell")){CKEDITOR.dom.element.clearAllMarkers(k);g=n;break a}CKEDITOR.dom.element.clearAllMarkers(k);f=d[0].getParent();(f=f.getPrevious())?g=f.getLast():(f=d[g].getParent(),g=(f=f.getNext())?f.getChild(0):null)}b.reset();for(b= d.length-1;0<=b;b--)h(d[b]);g?m(g,!0):e&&(c[0].moveToPosition(e,CKEDITOR.POSITION_BEFORE_START),c[0].select(),e.remove())}else b instanceof CKEDITOR.dom.element&&(c=b.getParent(),1==c.getChildCount()?c.remove():b.remove())}function m(a,b){var c=a.getDocument(),d=CKEDITOR.document;CKEDITOR.env.ie&&10==CKEDITOR.env.version&&(d.focus(),c.focus());c=new CKEDITOR.dom.range(c);c["moveToElementEdit"+(b?"End":"Start")](a)||(c.selectNodeContents(a),c.collapse(b?!1:!0));c.select(!0)}function e(a,b,c){a=a[b]; if("undefined"==typeof c)return a;for(b=0;a&&bg.length)||(f=b.getCommonAncestor())&&f.type==CKEDITOR.NODE_ELEMENT&&f.is("table"))return!1;var h;b=g[0];f=b.getAscendant("table");var k=CKEDITOR.tools.buildTableMap(f),l=k.length,m=k[0].length,n=b.getParent().$.rowIndex,q=e(k,n,b);if(c){var p;try{var u=parseInt(b.getAttribute("rowspan"),10)||1; h=parseInt(b.getAttribute("colspan"),10)||1;p=k["up"==c?n-u:"down"==c?n+u:n]["left"==c?q-h:"right"==c?q+h:q]}catch(y){return!1}if(!p||b.$==p)return!1;g["up"==c||"left"==c?"unshift":"push"](new CKEDITOR.dom.element(p))}c=b.getDocument();var L=n,u=p=0,G=!d&&new CKEDITOR.dom.documentFragment(c),D=0;for(c=0;c=m?b.removeAttribute("rowSpan"):b.$.rowSpan=p;p>=l?b.removeAttribute("colSpan"):b.$.colSpan=u;d=new CKEDITOR.dom.nodeList(f.$.rows);g=d.count();for(c=g-1;0<=c;c--)f=d.getItem(c),f.$.cells.length||(f.remove(),g++);return b} function q(b,c){var d=a(b);if(1l){g.insertBefore(new CKEDITOR.dom.element(q));break}else q=null;q||f.append(g)}else for(m=n=1,f=g.clone(),f.insertAfter(g),f.append(g=d.clone()), q=e(h,k),l=0;lc);n++){k[l+n]||(k[l+n]=[]);for(var q=0;q=d)break}}return k},function(){function a(a){return CKEDITOR.plugins.widget&&CKEDITOR.plugins.widget.isDomWidget(a)}function f(a, b){var c=a.getAscendant("table"),d=b.getAscendant("table"),e=CKEDITOR.tools.buildTableMap(c),g=m(a),f=m(b),h=[],k={},l,n;c.contains(d)&&(b=b.getAscendant({td:1,th:1}),f=m(b));g>f&&(c=g,g=f,f=c,c=a,a=b,b=c);for(c=0;cn&&(c=l,l=n,n=c);for(c=g;c<=f;c++)for(g=l;g<=n;g++)d=new CKEDITOR.dom.element(e[c][g]),d.$&&!d.getCustomData("selected_cell")&&(h.push(d),CKEDITOR.dom.element.setMarker(k,d,"selected_cell", !0));CKEDITOR.dom.element.clearAllMarkers(k);return h}function b(a){if(a)return a=a.clone(),a.enlarge(CKEDITOR.ENLARGE_ELEMENT),(a=a.getEnclosedNode())&&a.is&&a.is(CKEDITOR.dtd.$tableContent)}function c(a){return(a=a.editable().findOne(".cke_table-faked-selection"))&&a.getAscendant("table")}function d(a,b){var c=a.editable().find(".cke_table-faked-selection"),d=a.editable().findOne("[data-cke-table-faked-selection-table]"),e;a.fire("lockSnapshot");a.editable().removeClass("cke_table-faked-selection-editor"); for(e=0;eb.count()||(b=f(b.getItem(0),b.getItem(b.count()-1)), l(a,b))}function g(b,c,e){var g=r(b.getSelection(!0));c=c.is("table")?null:c;var h;(h=v.active&&!v.first)&&!(h=c)&&(h=b.getSelection().getRanges(),h=1CKEDITOR.env.version,l=a.blockless||CKEDITOR.env.ie?"span":"div",m,n,p,q;g.getById("cke_table_copybin")||(m=g.createElement(l),n=g.createElement(l),n.setAttributes({id:"cke_table_copybin","data-cke-temp":"1"}),m.setStyles({position:"absolute",width:"1px", height:"1px",overflow:"hidden"}),m.setStyle("ltr"==a.config.contentsLangDirection?"left":"right","-5000px"),m.setHtml(a.getSelectedHtml(!0)),a.fire("lockSnapshot"),n.append(m),a.editable().append(n),q=a.on("selectionChange",c,null,null,0),k&&(p=h.scrollTop),f.selectNodeContents(m),f.select(),k&&(h.scrollTop=p),setTimeout(function(){n.remove();d.selectBookmarks(e);q.removeListener();a.fire("unlockSnapshot");b&&(a.extractSelectedHtml(),a.fire("saveSnapshot"))},100))}function y(a){var b=a.editor||a.sender.editor, c=b.getSelection();c.isInTable()&&(c.getRanges()[0]._getTableElement({table:1}).hasAttribute("data-cke-tableselection-ignored")||q(b,"cut"===a.name))}function u(a){this._reset();a&&this.setSelectedCells(a)}function p(a,b,c){a.on("beforeCommandExec",function(c){-1!==CKEDITOR.tools.array.indexOf(b,c.data.name)&&(c.data.selectedCells=r(a.getSelection()))});a.on("afterCommandExec",function(d){-1!==CKEDITOR.tools.array.indexOf(b,d.data.name)&&c(a,d.data)})}var v={active:!1},w,r,z,t,x;u.prototype={};u.prototype._reset= function(){this.cells={first:null,last:null,all:[]};this.rows={first:null,last:null}};u.prototype.setSelectedCells=function(a){this._reset();a=a.slice(0);this._arraySortByDOMOrder(a);this.cells.all=a;this.cells.first=a[0];this.cells.last=a[a.length-1];this.rows.first=a[0].getAscendant("tr");this.rows.last=this.cells.last.getAscendant("tr")};u.prototype.getTableMap=function(){var a=z(this.cells.first),b;a:{b=this.cells.last;var c=b.getAscendant("table"),d=m(b),c=CKEDITOR.tools.buildTableMap(c),e;for(e= 0;e=a)return;for(var d=this.cells.first.$.cellIndex,e=this.cells.last.$.cellIndex,g=c?[]:this.cells.all,f,h=0;h=d&&a.$.cellIndex<=e}),g=b?f.concat(g):g.concat(f);this.setSelectedCells(g)};u.prototype.insertColumn=function(a){function b(a){a=m(a);return a>=e&&a<=g}if("undefined"===typeof a)a=1;else if(0>=a)return;for(var c=this.cells,d=c.all,e=m(c.first),g=m(c.last),c=0;cg)l[0].moveToElementEditablePosition(k?m:n,!k),h.selectRanges([l[0]]); else if(13!==g||13===f||f===CKEDITOR.SHIFT+13){for(e=0;eCKEDITOR.env.version)},onLoad:function(){w=CKEDITOR.plugins.tabletools;r=w.getSelectedCells;z=w.getCellColIndex;t=w.insertRow;x=w.insertColumn; CKEDITOR.document.appendStyleSheet(this.path+"styles/tableselection.css")},init:function(a){this.isSupportedEnvironment()&&(a.addContentsCss&&a.addContentsCss(this.path+"styles/tableselection.css"),a.on("contentDom",function(){var b=a.editable(),c=b.isInline()?b:a.document,d={editor:a};b.attachListener(c,"mousedown",e,null,d);b.attachListener(c,"mousemove",e,null,d);b.attachListener(c,"mouseup",e,null,d);b.attachListener(b,"dragstart",n);b.attachListener(a,"selectionCheck",h);CKEDITOR.plugins.tableselection.keyboardIntegration(a); CKEDITOR.plugins.clipboard&&!CKEDITOR.plugins.clipboard.isCustomCopyCutSupported&&(b.attachListener(b,"cut",y),b.attachListener(b,"copy",y))}),a.on("paste",A.onPaste,A),p(a,"rowInsertBefore rowInsertAfter columnInsertBefore columnInsertAfter cellInsertBefore cellInsertAfter".split(" "),function(a,b){l(a,b.selectedCells)}),p(a,["cellMerge","cellMergeRight","cellMergeDown"],function(a,b){l(a,[b.commandData.cell])}),p(a,["cellDelete"],function(a){d(a,!0)}))}})}(),"use strict",function(){var a=[CKEDITOR.CTRL+ 90,CKEDITOR.CTRL+89,CKEDITOR.CTRL+CKEDITOR.SHIFT+90],f={8:1,46:1};CKEDITOR.plugins.add("undo",{init:function(c){function d(a){e.enabled&&!1!==a.data.command.canUndo&&e.save()}function f(){e.enabled=c.readOnly?!1:"wysiwyg"==c.mode;e.onChange()}var e=c.undoManager=new b(c),k=e.editingHandler=new l(e),q=c.addCommand("undo",{exec:function(){e.undo()&&(c.selectionChange(),this.fire("afterUndo"))},startDisabled:!0,canUndo:!1}),y=c.addCommand("redo",{exec:function(){e.redo()&&(c.selectionChange(),this.fire("afterRedo"))}, startDisabled:!0,canUndo:!1});c.setKeystroke([[a[0],"undo"],[a[1],"redo"],[a[2],"redo"]]);e.onChange=function(){q.setState(e.undoable()?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED);y.setState(e.redoable()?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED)};c.on("beforeCommandExec",d);c.on("afterCommandExec",d);c.on("saveSnapshot",function(a){e.save(a.data&&a.data.contentOnly)});c.on("contentDom",k.attachListeners,k);c.on("instanceReady",function(){c.fire("saveSnapshot")});c.on("beforeModeUnload", function(){"wysiwyg"==c.mode&&e.save(!0)});c.on("mode",f);c.on("readOnly",f);c.ui.addButton&&(c.ui.addButton("Undo",{label:c.lang.undo.undo,command:"undo",toolbar:"undo,10"}),c.ui.addButton("Redo",{label:c.lang.undo.redo,command:"redo",toolbar:"undo,20"}));c.resetUndo=function(){e.reset();c.fire("saveSnapshot")};c.on("updateSnapshot",function(){e.currentImage&&e.update()});c.on("lockSnapshot",function(a){a=a.data;e.lock(a&&a.dontUpdate,a&&a.forceUpdate)});c.on("unlockSnapshot",e.unlock,e)}});CKEDITOR.plugins.undo= {};var b=CKEDITOR.plugins.undo.UndoManager=function(a){this.strokesRecorded=[0,0];this.locked=null;this.previousKeyGroup=-1;this.limit=a.config.undoStackSize||20;this.strokesLimit=25;this.editor=a;this.reset()};b.prototype={type:function(a,c){var d=b.getKeyGroup(a),e=this.strokesRecorded[d]+1;c=c||e>=this.strokesLimit;this.typing||(this.hasUndo=this.typing=!0,this.hasRedo=!1,this.onChange());c?(e=0,this.editor.fire("saveSnapshot")):this.editor.fire("change");this.strokesRecorded[d]=e;this.previousKeyGroup= d},keyGroupChanged:function(a){return b.getKeyGroup(a)!=this.previousKeyGroup},reset:function(){this.snapshots=[];this.index=-1;this.currentImage=null;this.hasRedo=this.hasUndo=!1;this.locked=null;this.resetType()},resetType:function(){this.strokesRecorded=[0,0];this.typing=!1;this.previousKeyGroup=-1},refreshState:function(){this.hasUndo=!!this.getNextImage(!0);this.hasRedo=!!this.getNextImage(!1);this.resetType();this.onChange()},save:function(a,b,d){var e=this.editor;if(this.locked||"ready"!=e.status|| "wysiwyg"!=e.mode)return!1;var f=e.editable();if(!f||"ready"!=f.status)return!1;f=this.snapshots;b||(b=new c(e));if(!1===b.contents)return!1;if(this.currentImage)if(b.equalsContent(this.currentImage)){if(a||b.equalsSelection(this.currentImage))return!1}else!1!==d&&e.fire("change");f.splice(this.index+1,f.length-this.index-1);f.length==this.limit&&f.shift();this.index=f.push(b)-1;this.currentImage=b;!1!==d&&this.refreshState();return!0},restoreImage:function(a){var b=this.editor,c;a.bookmarks&&(b.focus(), c=b.getSelection());this.locked={level:999};this.editor.loadSnapshot(a.contents);a.bookmarks?c.selectBookmarks(a.bookmarks):CKEDITOR.env.ie&&(c=this.editor.document.getBody().$.createTextRange(),c.collapse(!0),c.select());this.locked=null;this.index=a.index;this.currentImage=this.snapshots[this.index];this.update();this.refreshState();b.fire("change")},getNextImage:function(a){var b=this.snapshots,c=this.currentImage,d;if(c)if(a)for(d=this.index-1;0<=d;d--){if(a=b[d],!c.equalsContent(a))return a.index= d,a}else for(d=this.index+1;d=this.undoManager.strokesLimit&&(this.undoManager.type(a.keyCode,!0),this.keyEventsStack.resetInputs())}},onKeyup:function(a){var d=this.undoManager; a=a.data.getKey();var f=this.keyEventsStack.getTotalInputs();this.keyEventsStack.remove(a);if(!(b.ieFunctionalKeysBug(a)&&this.lastKeydownImage&&this.lastKeydownImage.equalsContent(new c(d.editor,!0))))if(0=this.rect.right||a<=this.rect.top||a>=this.rect.bottom)&&this.hideVisible();(0>=b||b>=this.winTopPane.width||0>=a||a>=this.winTopPane.height)&&this.hideVisible()},this);c.attachListener(a,"resize",f);c.attachListener(a,"mode",k);a.on("destroy",k);this.lineTpl=(new CKEDITOR.template('\x3cdiv data-cke-lineutils-line\x3d"1" class\x3d"cke_reset_all" style\x3d"{lineStyle}"\x3e\x3cspan style\x3d"{tipLeftStyle}"\x3e\x26nbsp;\x3c/span\x3e\x3cspan style\x3d"{tipRightStyle}"\x3e\x26nbsp;\x3c/span\x3e\x3c/div\x3e')).output({lineStyle:CKEDITOR.tools.writeCssText(CKEDITOR.tools.extend({}, l,this.lineStyle,!0)),tipLeftStyle:CKEDITOR.tools.writeCssText(CKEDITOR.tools.extend({},d,{left:"0px","border-left-color":"red","border-width":"6px 0 6px 6px"},this.tipCss,this.tipLeftStyle,!0)),tipRightStyle:CKEDITOR.tools.writeCssText(CKEDITOR.tools.extend({},d,{right:"0px","border-right-color":"red","border-width":"6px 6px 6px 0"},this.tipCss,this.tipRightStyle,!0))})}function c(a){var b;if(b=a&&a.type==CKEDITOR.NODE_ELEMENT)b=!(k[a.getComputedStyle("float")]||k[a.getAttribute("align")]);return b&& !g[a.getComputedStyle("position")]}CKEDITOR.plugins.add("lineutils");CKEDITOR.LINEUTILS_BEFORE=1;CKEDITOR.LINEUTILS_AFTER=2;CKEDITOR.LINEUTILS_INSIDE=4;a.prototype={start:function(a){var b=this,c=this.editor,d=this.doc,f,g,k,l,v=CKEDITOR.tools.eventsBuffer(50,function(){c.readOnly||"wysiwyg"!=c.mode||(b.relations={},(g=d.$.elementFromPoint(k,l))&&g.nodeType&&(f=new CKEDITOR.dom.element(g),b.traverseSearch(f),isNaN(k+l)||b.pixelSearch(f,k,l),a&&a(b.relations,k,l)))});this.listener=this.editable.attachListener(this.target, "mousemove",function(a){k=a.data.$.clientX;l=a.data.$.clientY;v.input()});this.editable.attachListener(this.inline?this.editable:this.frame,"mouseout",function(){v.reset()})},stop:function(){this.listener&&this.listener.removeListener()},getRange:function(){var a={};a[CKEDITOR.LINEUTILS_BEFORE]=CKEDITOR.POSITION_BEFORE_START;a[CKEDITOR.LINEUTILS_AFTER]=CKEDITOR.POSITION_AFTER_END;a[CKEDITOR.LINEUTILS_INSIDE]=CKEDITOR.POSITION_AFTER_START;return function(b){var c=this.editor.createRange();c.moveToPosition(this.relations[b.uid].element, a[b.type]);return c}}(),store:function(){function a(b,c,d){var f=b.getUniqueId();f in d?d[f].type|=c:d[f]={element:b,type:c}}return function(b,d){var f;d&CKEDITOR.LINEUTILS_AFTER&&c(f=b.getNext())&&f.isVisible()&&(a(f,CKEDITOR.LINEUTILS_BEFORE,this.relations),d^=CKEDITOR.LINEUTILS_AFTER);d&CKEDITOR.LINEUTILS_INSIDE&&c(f=b.getFirst())&&f.isVisible()&&(a(f,CKEDITOR.LINEUTILS_BEFORE,this.relations),d^=CKEDITOR.LINEUTILS_INSIDE);a(b,d,this.relations)}}(),traverseSearch:function(a){var b,d,f;do if(f=a.$["data-cke-expando"], !(f&&f in this.relations)){if(a.equals(this.editable))break;if(c(a))for(b in this.lookups)(d=this.lookups[b](a))&&this.store(a,d)}while((!a||a.type!=CKEDITOR.NODE_ELEMENT||"true"!=a.getAttribute("contenteditable"))&&(a=a.getParent()))},pixelSearch:function(){function a(d,f,g,h,k){for(var l=0,v;k(g);){g+=h;if(25==++l)break;if(v=this.doc.$.elementFromPoint(f,g))if(v==d)l=0;else if(b(d,v)&&(l=0,c(v=new CKEDITOR.dom.element(v))))return v}}var b=CKEDITOR.env.ie||CKEDITOR.env.webkit?function(a,b){return a.contains(b)}: function(a,b){return!!(a.compareDocumentPosition(b)&16)};return function(b,d,f){var g=this.win.getViewPaneSize().height,k=a.call(this,b.$,d,f,-1,function(a){return 0this.rect.bottom)return!1;this.inline? f.left=c.elementRect.left-this.rect.relativeX:(0[^<]*e});0>f&&(f=a._.upcasts.length);a._.upcasts.splice(f,0,[CKEDITOR.tools.bind(b,c),c.name,e])}var e=b.upcast,f=b.upcastPriority||10;e&&("string"==typeof e?d(c, b,f):d(e,b,f))}function l(a,b){a.focused=null;if(b.isInited()){var c=b.editor.checkDirty();a.fire("widgetBlurred",{widget:b});b.setFocused(!1);!c&&b.editor.resetDirty()}}function k(a){a=a.data;if("wysiwyg"==this.editor.mode){var b=this.editor.editable(),c=this.instances,d,e,g,h;if(b){for(d in c)c[d].isReady()&&!b.contains(c[d].wrapper)&&this.destroy(c[d],!0);if(a&&a.initOnlyNew)c=this.initOnAll();else{var k=b.find(".cke_widget_wrapper"),c=[];d=0;for(e=k.count();dCKEDITOR.tools.indexOf(b,a)&&c.push(a);a=CKEDITOR.tools.indexOf(d,a);0<=a&&d.splice(a,1);return this},focus:function(a){e=a;return this}, commit:function(){var f=a.focused!==e,g,h;a.editor.fire("lockSnapshot");for(f&&(g=a.focused)&&l(a,g);g=d.pop();)b.splice(CKEDITOR.tools.indexOf(b,g),1),g.isInited()&&(h=g.editor.checkDirty(),g.setSelected(!1),!h&&g.editor.resetDirty());f&&e&&(h=a.editor.checkDirty(),a.focused=e,a.fire("widgetFocused",{widget:e}),e.setFocused(!0),!h&&a.editor.resetDirty());for(;g=c.pop();)b.push(g),g.setSelected(!0);a.editor.fire("unlockSnapshot")}}}function I(a,b,c){var d=0;b=L(b);var e=a.data.classes||{},f;if(b){for(e= CKEDITOR.tools.clone(e);f=b.pop();)c?e[f]||(d=e[f]=1):e[f]&&(delete e[f],d=1);d&&a.setData("classes",e)}}function J(a){a.cancel()}function E(a,b){var c=a.editor,d=c.document,e=CKEDITOR.env.edge&&16<=CKEDITOR.env.version;if(!d.getById("cke_copybin")){var f=!c.blockless&&!CKEDITOR.env.ie||e?"div":"span",e=d.createElement(f),g=d.createElement(f),f=CKEDITOR.env.ie&&9>CKEDITOR.env.version;g.setAttributes({id:"cke_copybin","data-cke-temp":"1"});e.setStyles({position:"absolute",width:"1px",height:"1px", overflow:"hidden"});e.setStyle("ltr"==c.config.contentsLangDirection?"left":"right","-5000px");var h=c.createRange();h.setStartBefore(a.wrapper);h.setEndAfter(a.wrapper);e.setHtml('\x3cspan data-cke-copybin-start\x3d"1"\x3e​\x3c/span\x3e'+c.editable().getHtmlFromRange(h).getHtml()+'\x3cspan data-cke-copybin-end\x3d"1"\x3e​\x3c/span\x3e');c.fire("saveSnapshot");c.fire("lockSnapshot");g.append(e);c.editable().append(g);var k=c.on("selectionChange",J,null,null,0),l=a.repository.on("checkSelection",J, null,null,0);if(f)var m=d.getDocumentElement().$,n=m.scrollTop;h=c.createRange();h.selectNodeContents(e);h.select();f&&(m.scrollTop=n);setTimeout(function(){b||a.focus();g.remove();k.removeListener();l.removeListener();c.fire("unlockSnapshot");b&&!c.readOnly&&(a.repository.del(a),c.fire("saveSnapshot"))},100)}}function L(a){return(a=(a=a.getDefinition().attributes)&&a["class"])?a.split(/\s+/):null}function G(){var a=CKEDITOR.document.getActive(),b=this.editor,c=b.editable();(c.isInline()?c:b.document.getWindow().getFrame()).equals(a)&& b.focusManager.focus(c)}function D(){CKEDITOR.env.gecko&&this.editor.unlockSelection();CKEDITOR.env.webkit||(this.editor.forceNextSelectionCheck(),this.editor.selectionChange(1))}function N(a){var b=null;a.on("data",function(){var a=this.data.classes,c;if(b!=a){for(c in b)a&&a[c]||this.removeClass(c);for(c in a)this.addClass(c);b=a}})}function Q(a){a.on("data",function(){if(a.wrapper){var b=this.getLabel?this.getLabel():this.editor.lang.widget.label.replace(/%1/,this.pathName||this.element.getName()); a.wrapper.setAttribute("role","region");a.wrapper.setAttribute("aria-label",b)}},null,null,9999)}function O(a){if(a.draggable){var b=a.editor,c=a.wrapper.getLast(f.isDomDragHandlerContainer),d;c?d=c.findOne("img"):(c=new CKEDITOR.dom.element("span",b.document),c.setAttributes({"class":"cke_reset cke_widget_drag_handler_container",style:"background:rgba(220,220,220,0.5);background-image:url("+b.plugins.widget.path+"images/handle.png)"}),d=new CKEDITOR.dom.element("img",b.document),d.setAttributes({"class":"cke_reset cke_widget_drag_handler", "data-cke-widget-drag-handler":"1",src:CKEDITOR.tools.transparentImageData,width:15,title:b.lang.widget.move,height:15,role:"presentation"}),a.inline&&d.setAttribute("draggable","true"),c.append(d),a.wrapper.append(c));a.wrapper.on("dragover",function(a){a.data.preventDefault()});a.wrapper.on("mouseenter",a.updateDragHandlerPosition,a);setTimeout(function(){a.on("data",a.updateDragHandlerPosition,a)},50);if(!a.inline&&(d.on("mousedown",K,a),CKEDITOR.env.ie&&9>CKEDITOR.env.version))d.on("dragstart", function(a){a.data.preventDefault(!0)});a.dragHandlerContainer=c}}function K(a){function b(){var c;for(p.reset();c=h.pop();)c.removeListener();var d=k;c=a.sender;var e=this.repository.finder,f=this.repository.liner,g=this.editor,l=this.editor.editable();CKEDITOR.tools.isEmpty(f.visible)||(d=e.getRange(d[0]),this.focus(),g.fire("drop",{dropRange:d,target:d.startContainer}));l.removeClass("cke_widget_dragging");f.hideVisible();g.fire("dragend",{target:c})}if(CKEDITOR.tools.getMouseButton(a)===CKEDITOR.MOUSE_BUTTON_LEFT){var c= this.repository.finder,d=this.repository.locator,e=this.repository.liner,f=this.editor,g=f.editable(),h=[],k=[],l,m;this.repository._.draggedWidget=this;var n=c.greedySearch(),p=CKEDITOR.tools.eventsBuffer(50,function(){l=d.locate(n);k=d.sort(m,1);k.length&&(e.prepare(n,l),e.placeLine(k[0]),e.cleanup())});g.addClass("cke_widget_dragging");h.push(g.on("mousemove",function(a){m=a.data.$.clientY;p.input()}));f.fire("dragstart",{target:a.sender});h.push(f.document.once("mouseup",b,this));g.isInline()|| h.push(CKEDITOR.document.once("mouseup",b,this))}}function W(a){var b,c,d=a.editables;a.editables={};if(a.editables)for(b in d)c=d[b],a.initEditable(b,"string"==typeof c?{selector:c}:c)}function R(a){if(a.mask){var b=a.wrapper.findOne(".cke_widget_mask");b||(b=new CKEDITOR.dom.element("img",a.editor.document),b.setAttributes({src:CKEDITOR.tools.transparentImageData,"class":"cke_reset cke_widget_mask"}),a.wrapper.append(b));a.mask=b}}function Z(a){if(a.parts){var b={},c,d;for(d in a.parts)c=a.wrapper.findOne(a.parts[d]), b[d]=c;a.parts=b}}function ha(a,b){X(a);Z(a);W(a);R(a);O(a);N(a);Q(a);if(CKEDITOR.env.ie&&9>CKEDITOR.env.version)a.wrapper.on("dragstart",function(b){var c=b.data.getTarget();f.getNestedEditable(a,c)||a.inline&&f.isDomDragHandler(c)||b.data.preventDefault()});a.wrapper.removeClass("cke_widget_new");a.element.addClass("cke_widget_element");a.on("key",function(b){b=b.data.keyCode;if(13==b)a.edit();else{if(b==CKEDITOR.CTRL+67||b==CKEDITOR.CTRL+88){E(a,b==CKEDITOR.CTRL+88);return}if(b in V||CKEDITOR.CTRL& b||CKEDITOR.ALT&b)return}return!1},null,null,999);a.on("doubleclick",function(b){a.edit()&&b.cancel()});if(b.data)a.on("data",b.data);if(b.edit)a.on("edit",b.edit)}function X(a){(a.wrapper=a.element.getParent()).setAttribute("data-cke-widget-id",a.id)}function U(a){a.element.data("cke-widget-data",encodeURIComponent(JSON.stringify(a.data)))}function ba(){function a(){}function b(a,c,d){return d&&this.checkElement(a)?(a=d.widgets.getByElement(a,!0))&&a.checkStyleActive(this):!1}function c(a){function b(a, c,d){for(var e=a.length,f=0;f)?(?:<(?:div|span)(?: style="[^"]+")?>)?]*data-cke-copybin-start="1"[^>]*>.?<\/span>([\s\S]+)]*data-cke-copybin-end="1"[^>]*>.?<\/span>(?:<\/(?:div|span)>)?(?:<\/(?:div|span)>)?$/i,V={37:1,38:1,39:1,40:1,8:1,46:1};V[CKEDITOR.SHIFT+121]=1;CKEDITOR.plugins.widget=f;f.repository=a;f.nestedEditable=b}(),function(){function a(a,c,d){this.editor= a;this.notification=null;this._message=new CKEDITOR.template(c);this._singularMessage=d?new CKEDITOR.template(d):null;this._tasks=[];this._doneTasks=this._doneWeights=this._totalWeights=0}function f(a){this._weight=a||1;this._doneWeight=0;this._isCanceled=!1}CKEDITOR.plugins.add("notificationaggregator",{requires:"notification"});a.prototype={createTask:function(a){a=a||{};var c=!this.notification,d;c&&(this.notification=this._createNotification());d=this._addTask(a);d.on("updated",this._onTaskUpdate, this);d.on("done",this._onTaskDone,this);d.on("canceled",function(){this._removeTask(d)},this);this.update();c&&this.notification.show();return d},update:function(){this._updateNotification();this.isFinished()&&this.fire("finished")},getPercentage:function(){return 0===this.getTaskCount()?1:this._doneWeights/this._totalWeights},isFinished:function(){return this.getDoneTaskCount()===this.getTaskCount()},getTaskCount:function(){return this._tasks.length},getDoneTaskCount:function(){return this._doneTasks}, _updateNotification:function(){this.notification.update({message:this._getNotificationMessage(),progress:this.getPercentage()})},_getNotificationMessage:function(){var a=this.getTaskCount(),c={current:this.getDoneTaskCount(),max:a,percentage:Math.round(100*this.getPercentage())};return(1==a&&this._singularMessage?this._singularMessage:this._message).output(c)},_createNotification:function(){return new CKEDITOR.plugins.notification(this.editor,{type:"progress"})},_addTask:function(a){a=new f(a.weight); this._tasks.push(a);this._totalWeights+=a._weight;return a},_removeTask:function(a){var c=CKEDITOR.tools.indexOf(this._tasks,a);-1!==c&&(a._doneWeight&&(this._doneWeights-=a._doneWeight),this._totalWeights-=a._weight,this._tasks.splice(c,1),this.update())},_onTaskUpdate:function(a){this._doneWeights+=a.data;this.update()},_onTaskDone:function(){this._doneTasks+=1;this.update()}};CKEDITOR.event.implementOn(a.prototype);f.prototype={done:function(){this.update(this._weight)},update:function(a){if(!this.isDone()&& !this.isCanceled()){a=Math.min(this._weight,a);var c=a-this._doneWeight;this._doneWeight=a;this.fire("updated",c);this.isDone()&&this.fire("done")}},cancel:function(){this.isDone()||this.isCanceled()||(this._isCanceled=!0,this.fire("canceled"))},isDone:function(){return this._weight===this._doneWeight},isCanceled:function(){return this._isCanceled}};CKEDITOR.event.implementOn(f.prototype);CKEDITOR.plugins.notificationAggregator=a;CKEDITOR.plugins.notificationAggregator.task=f}(),"use strict",function(){CKEDITOR.plugins.add("uploadwidget", {requires:"widget,clipboard,filetools,notificationaggregator",init:function(a){a.filter.allow("*[!data-widget,!data-cke-upload-id]")},isSupportedEnvironment:function(){return CKEDITOR.plugins.clipboard.isFileApiSupported}});CKEDITOR.fileTools||(CKEDITOR.fileTools={});CKEDITOR.tools.extend(CKEDITOR.fileTools,{addUploadWidget:function(a,f,b){var c=CKEDITOR.fileTools,d=a.uploadRepository,l=b.supportedTypes?10:20;if(b.fileToElement)a.on("paste",function(b){b=b.data;var g=a.widgets.registered[f],h=b.dataTransfer, l=h.getFilesCount(),e=g.loadMethod||"loadAndUpload",n,q;if(!b.dataValue&&l)for(q=0;q=a&&(a="0"+a);return String(a)}function f(c){var d=new Date,d=[d.getFullYear(),d.getMonth()+1,d.getDate(),d.getHours(),d.getMinutes(),d.getSeconds()];b+=1;return"image-"+CKEDITOR.tools.array.map(d,a).join("")+"-"+b+"."+c}var b=0;CKEDITOR.plugins.add("uploadimage", {requires:"uploadwidget",onLoad:function(){CKEDITOR.addCss(".cke_upload_uploading img{opacity: 0.3}")},isSupportedEnvironment:function(){return CKEDITOR.plugins.clipboard.isFileApiSupported},init:function(a){if(this.isSupportedEnvironment()){var b=CKEDITOR.fileTools,l=b.getUploadUrl(a.config,"image");l&&(b.addUploadWidget(a,"uploadimage",{supportedTypes:/image\/(jpeg|png|gif|bmp)/,uploadUrl:l,fileToElement:function(){var a=new CKEDITOR.dom.element("img");a.setAttribute("src","data:image/gif;base64,R0lGODlhDgAOAIAAAAAAAP///yH5BAAAAAAALAAAAAAOAA4AAAIMhI+py+0Po5y02qsKADs\x3d"); return a},parts:{img:"img"},onUploading:function(a){this.parts.img.setAttribute("src",a.data)},onUploaded:function(a){var b=this.parts.img.$;this.replaceWith('\x3cimg src\x3d"'+a.url+'" width\x3d"'+(a.responseData.width||b.naturalWidth)+'" height\x3d"'+(a.responseData.height||b.naturalHeight)+'"\x3e')}}),a.on("paste",function(k){if(k.data.dataValue.match(/f.version||f.quirks))};"undefined"==typeof a.plugins.scayt&&a.ui.addButton&&a.ui.addButton("SpellChecker",{label:a.lang.wsc.toolbar,click:function(a){var c=a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?a.container.getText(): a.document.getBody().getText();(c=c.replace(/\s/g,""))?a.execCommand("checkspell"):alert("Nothing to check!")},toolbar:"spellchecker,10"});CKEDITOR.dialog.add("checkspell",this.path+(CKEDITOR.env.ie&&7>=CKEDITOR.env.version?"dialogs/wsc_ie.js":window.postMessage?"dialogs/wsc.js":"dialogs/wsc_ie.js"))}}),function(){function a(a){function b(a){var c=!1;e.attachListener(e,"keydown",function(){var b=g.getBody().getElementsByTag(a);if(!c){for(var d=0;dCKEDITOR.env.version&&c.enterMode!=CKEDITOR.ENTER_DIV&&b("div");if(CKEDITOR.env.webkit||CKEDITOR.env.ie&&10this.$.offsetHeight){var d=c.createRange();d[33==b?"moveToElementEditStart": "moveToElementEditEnd"](this);d.select();a.data.preventDefault()}});CKEDITOR.env.ie&&this.attachListener(g,"blur",function(){try{g.$.selection.empty()}catch(a){}});CKEDITOR.env.iOS&&this.attachListener(g,"touchend",function(){a.focus()});h=c.document.getElementsByTag("title").getItem(0);h.data("cke-title",h.getText());CKEDITOR.env.ie&&(c.document.$.title=this._.docTitle);CKEDITOR.tools.setTimeout(function(){"unloaded"==this.status&&(this.status="ready");c.fire("contentDom");this._.isPendingFocus&& (c.focus(),this._.isPendingFocus=!1);setTimeout(function(){c.fire("dataReady")},0)},0,this)}function f(a){function b(){var f;a.editable().attachListener(a,"selectionChange",function(){var b=a.getSelection().getSelectedElement();b&&(f&&(f.detachEvent("onresizestart",c),f=null),b.$.attachEvent("onresizestart",c),f=b.$)})}function c(a){a.returnValue=!1}if(CKEDITOR.env.gecko)try{var f=a.document.$;f.execCommand("enableObjectResizing",!1,!a.config.disableObjectResizing);f.execCommand("enableInlineTableEditing", !1,!a.config.disableNativeTableHandles)}catch(h){}else CKEDITOR.env.ie&&11>CKEDITOR.env.version&&a.config.disableObjectResizing&&b(a)}function b(){var a=[];if(8<=CKEDITOR.document.$.documentMode){a.push("html.CSS1Compat [contenteditable\x3dfalse]{min-height:0 !important}");var b=[],c;for(c in CKEDITOR.dtd.$removeEmpty)b.push("html.CSS1Compat "+c+"[contenteditable\x3dfalse]");a.push(b.join(",")+"{display:inline-block}")}else CKEDITOR.env.gecko&&(a.push("html{height:100% !important}"),a.push("img:-moz-broken{-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}")); a.push("html{cursor:text;*cursor:auto}");a.push("img,input,textarea{cursor:default}");return a.join("\n")}var c;CKEDITOR.plugins.add("wysiwygarea",{init:function(a){a.config.fullPage&&a.addFeature({allowedContent:"html head title; style [media,type]; body (*)[id]; meta link [*]",requiredContent:"body"});a.addMode("wysiwyg",function(b){function f(e){e&&e.removeListener();a.editable(new c(a,h.$.contentWindow.document.body));a.setData(a.getData(1),b)}var g="document.open();"+(CKEDITOR.env.ie?"("+CKEDITOR.tools.fixDomain+ ")();":"")+"document.close();",g=CKEDITOR.env.air?"javascript:void(0)":CKEDITOR.env.ie&&!CKEDITOR.env.edge?"javascript:void(function(){"+encodeURIComponent(g)+"}())":"",h=CKEDITOR.dom.element.createFromHtml('\x3ciframe src\x3d"'+g+'" frameBorder\x3d"0"\x3e\x3c/iframe\x3e');h.setStyles({width:"100%",height:"100%"});h.addClass("cke_wysiwyg_frame").addClass("cke_reset");g=a.ui.space("contents");g.append(h);var m=CKEDITOR.env.ie&&!CKEDITOR.env.edge||CKEDITOR.env.gecko;if(m)h.on("load",f);var e=a.title, n=a.fire("ariaEditorHelpLabel",{}).label;e&&(CKEDITOR.env.ie&&n&&(e+=", "+n),h.setAttribute("title",e));if(n){var e=CKEDITOR.tools.getNextId(),q=CKEDITOR.dom.element.createFromHtml('\x3cspan id\x3d"'+e+'" class\x3d"cke_voice_label"\x3e'+n+"\x3c/span\x3e");g.append(q,1);h.setAttribute("aria-describedby",e)}a.on("beforeModeUnload",function(a){a.removeListener();q&&q.remove()});h.setAttributes({tabIndex:a.tabIndex,allowTransparency:"true"});!m&&f();a.fire("ariaWidget",h)})}});CKEDITOR.editor.prototype.addContentsCss= function(a){var b=this.config,c=b.contentsCss;CKEDITOR.tools.isArray(c)||(b.contentsCss=c?[c]:[]);b.contentsCss.push(a)};c=CKEDITOR.tools.createClass({$:function(){this.base.apply(this,arguments);this._.frameLoadedHandler=CKEDITOR.tools.addFunction(function(b){CKEDITOR.tools.setTimeout(a,0,this,b)},this);this._.docTitle=this.getWindow().getFrame().getAttribute("title")},base:CKEDITOR.editable,proto:{setData:function(a,c){var f=this.editor;if(c)this.setHtml(a),this.fixInitialSelection(),f.fire("dataReady"); else{this._.isLoadingData=!0;f._.dataStore={id:1};var g=f.config,h=g.fullPage,m=g.docType,e=CKEDITOR.tools.buildStyleHtml(b()).replace(/
`); $('.wrapper').append(skinModal); /** * skin opt */ $('#changeSkin').on('click', function(){ $('#skinModal').modal({backdrop: true, keyboard: false}).modal('show'); }); /** * skin list */ var skins = [ "skin-blue", "skin-black", "skin-purple", "skin-green", "skin-red", "skin-yellow", "skin-blue-light", "skin-black-light", "skin-purple-light", "skin-green-light", "skin-red-light", "skin-yellow-light" ]; /** * skin change */ $("[data-skin]").on('click', function(e) { var skin = $(this).data('skin'); $.each(skins, function (i) { $("body").removeClass(skins[i]); }); $("body").addClass(skin); store('admin_skin', skin); return false; }); /** * skin init */ let skin = loadStore('admin_skin'); if (skin) { $.each(skins, function (i) { $("body").removeClass(skins[i]); }); $("body").addClass(skin); } // ---------------------- localStorage ---------------------- /** * store */ function store(name, val) { if (typeof (Storage) !== "undefined") { localStorage.setItem(name, val); } else { window.alert('Please use a modern browser to properly view this template!'); } } /** * get */ function loadStore(name) { if (typeof (Storage) !== "undefined") { return localStorage.getItem(name); } else { window.alert('Please use a modern browser to properly view this template!'); } } // ---------------------- menu, sidebar-toggle ---------------------- // init menu speed $('.sidebar-menu').attr('data-animation-speed', 1); // init menu status let sidebar = loadStore('admin_sidebar_status'); if ( 'close' === sidebar ) { $('body').addClass('sidebar-collapse'); } else { $('body').removeClass('sidebar-collapse'); } // change menu status $('.sidebar-toggle').click(function(){ if ( 'close' === loadStore('admin_sidebar_status') ) { store('admin_sidebar_status', 'open') } else { store('admin_sidebar_status', 'close') } }); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/biz/common/admin.tab.css ================================================ /* admin tab start */ html, body { height: 100% !important; min-height: 0px !important; } body .wrapper{ height: 100% !important; min-height: 0px !important; } body .content-wrapper{ /*for footer*/ height:calc(100% - 36px); } body .sidebar{ overflow: hidden; } .content-tabs { position: relative; height: 35px; background: #fafafa; line-height: 35px; } .content-tabs .roll-nav, .page-tabs-list { position: absolute; width: 40px; height: 35px; text-align: center; color: #999; z-index: 2; top: 0; } .content-tabs .roll-left { left: 0; border-right: solid 1px #eee; } .content-tabs .roll-right { right: 0; border-left: solid 1px #eee; } .content-tabs button { background: #fff; border: 0; height: 35px; width: 40px; outline: none; } .content-tabs button:hover { background: #fafafa; } nav.page-tabs { margin-left: 40px; width: 100000px; height: 35px; overflow: hidden; } nav.page-tabs .page-tabs-content { float: left; } .page-tabs a { display: block; float: left; border-right: solid 1px #eee; padding: 0 15px; } .page-tabs a i:hover { color: #c00; } .page-tabs a:hover, .content-tabs .roll-nav:hover { color: #777; background: #f2f2f2; cursor: pointer; } .roll-right.J_tabRight { right: 200px; } .roll-right.btn-group { right: 120px; width: 80px; padding: 0; } .roll-right.btn-group button { width: 80px; } .roll-right.J_tabExit, .roll-right.tabReload { right: 60px; background: #fff; height: 35px; width: 60px; outline: none; } .roll-right.J_tabExit, .roll-right.fullscreen { background: #fff; height: 35px; width: 60px; outline: none; } .dropdown-menu-right { left: auto; } #content-main { height: calc(100% - 50px); overflow: hidden; } .fixed-nav #content-main { height: calc(100% - 80px); overflow: hidden; } .content-tabs { height: 37px; border-bottom: solid 2px #d2d6de; } .page-tabs a { color: #999; } .page-tabs a i { color: #ccc; } .page-tabs a.active { background: #2f4050; color: #ccc; } .page-tabs a.active:hover, .page-tabs a.active i:hover { background: #2f4050; color: #fff; } /** 遮罩层 **/ .loaderbox { display: inline-block; min-width: 125px; padding: 10px; margin: 0 auto; color: #000 !important; font-size: 13px; font-weight: 400; text-align: center; vertical-align: middle; border: 1px solid #ddd; background-color: #eee; -webkit-border-radius: 2px; -moz-border-radius: 2px; -ms-border-radius: 2px; -o-border-radius: 2px; border-radius: 2px; -webkit-box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1); -moz-box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1); } .loaderbox .loading-activity { float: left; width: 18px; height: 18px; border: solid 2px transparent; border-top-color: #000; border-left-color: #000; border-radius: 10px; -webkit-animation: pace-spinner 400ms linear infinite; -moz-animation: pace-spinner 400ms linear infinite; -ms-animation: pace-spinner 400ms linear infinite; -o-animation: pace-spinner 400ms linear infinite; animation: pace-spinner 400ms linear infinite; } .dropdown-menu { min-width: 100px; } .main-footer { height: 37px; border-top: solid 2px #d2d6de; padding: 8px 20px 12px 20px; } /* admin tab end */ /* nprogress start */ #nprogress .bar { background: #FFFFFF !important; } #nprogress .peg { box-shadow: 0 0 10px #FFFFFF, 0 0 5px #FFFFFF !important; } #nprogress .spinner-icon { border-top-color: #FFFFFF !important; border-left-color: #FFFFFF !important; } /* nprogress end */ /* menu start */ .treeview-menu>li>a { padding-top: 8px; padding-bottom: 8px; } /* menu end */ ================================================ FILE: xxl-job-admin/src/main/resources/static/biz/common/admin.tab.js ================================================ /*! * Admin Tab for XXL-BOOT * ================ * * 1、menu: * - J_menuItem: 菜单 * 2、tab: * - J_tabLeft: 左移按扭 * - J_menuTabs: * - J_menuTab : 菜单Tab * - J_tabRight: 右移按扭 * - J_tabCloseOther: 关闭其他Tab * - J_tabCloseAll : 关闭所有Tab * 3、contont * - J_mainContent * - J_iframe : 页面iframe * * @author xuxueli * @repository https://github.com/xuxueli/xxl-boot */ (function($) { $.extend({ adminTab: { initTab: function(options) { /** * 点击菜单:打开Tab,展示Menu页面 */ $('.J_menuItem').on('click', function (){ // 获取Tab属性 let tabSrc = $(this).attr('href'); let tabName = $.trim($(this).text()); // open tab return openTab(tabSrc, tabName); }); /** * Tab关闭(x按钮):关闭菜单页面 */ $('.J_menuTabs').on('click', '.J_menuTab i', closeTab); /** * Tab单击(切换/激活):展示Tab页面 + 左侧菜单active更新 */ $('.J_menuTabs').on('click', '.J_menuTab', activeTab); /** * 选项卡-左移按扭:查看左侧隐藏的选项卡 */ $('.J_tabLeft').on('click', scrollTabLeft); /** * 选项卡-右移按扭:查看右侧隐藏的选项卡 */ $('.J_tabRight').on('click', scrollTabRight); /** * Tab-刷新按钮:刷新active的 Tab 页面 */ $('.tabReload').on('click', refreshTab); /** * 关闭其他选项卡 */ $('.J_tabCloseOther').on('click', closeOtherTabs); /** * 关闭当前选项卡 */ $('.tabCloseCurrent').on('click', tabCloseCurrent); /** * 关闭全部选项卡 */ $('.J_tabCloseAll').on('click', tabCloseAll); /** * 全屏显示 */ $('#fullScreen').on('click', function () { let currentHash = window.location.hash; $(document).toggleFullScreen(); // reset if (currentHash) { setTimeout(function (){ window.location.hash = currentHash; },50) } }); /** * 默认打开菜单Tab:优先尝试打开url路径TAB,兜底打开首个菜单 */ openDefaultTab(); }, openTab: function(options, isCloseCurrent) { // 当前页面是否关闭 if (isCloseCurrent) { tabCloseCurrent(); } // 打开Tab页面 return openTab(options.tabSrc, options.tabName); } } }); // -------------------- tab:open default -------------------- /** * 默认打开菜单Tab:初始化首页菜单,然后 尝试打开url路径TAB */ function openDefaultTab() { // load url let tabSrc = window.location.hash.slice(1); // 1、首页菜单:初始化 let $firstMenuItem = $(".J_menuItem:first"); if ($firstMenuItem.length > 0) { $firstMenuItem.click(); // 首页菜单特殊逻辑,不允许关闭 $('.J_menuTab[data-id="' + $firstMenuItem.attr('href') + '"] i').remove(); } // 2、URL匹配到菜单,初始化 if (tabSrc === '' || tabSrc === undefined || tabSrc === null) { // URL菜单路径不存在则pass return; } setTimeout(function (){ var $menuItem = $('.J_menuItem').filter('a[href$="' + decodeURI(tabSrc) + '"]'); if ($menuItem.length > 0) { // URL匹配到菜单,初始化 $menuItem.click(); return; } else { // 匹配失败,兜底直接打开 openTab(tabSrc, tabSrc); } }, 100) } // -------------------- tab:open、close -------------------- /** * 打开Tab:根据 url + 名称 * * @param tabSrc * @param tabName * @returns {boolean} */ function openTab(tabSrc, tabName) { // 0、valid dateurl if (tabSrc === undefined || $.trim(tabSrc).length === 0){ return false; } if (tabName === undefined || $.trim(tabName).length === 0){ tabName = tabSrc; } let tabNameShow = tabName.length > 15 ? tabName.substring(0, 15) + '...' : tabName; // 1、菜单Menu联动 + 页面锚点(hash参数)更新 activeMenuAndPath(tabSrc); // 2、匹配已存在Tab,切换/active展示 let tabExist = false; $('.J_menuTab').each(function () { // 匹配成功 if ($(this).data('id') === tabSrc) { // Tab是否展示,若否则切换展示 if (!$(this).hasClass('active')) { $(this).addClass('active').siblings('.J_menuTab').removeClass('active'); scrollToTab(this); // 显示Tab对应的内容区 $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') === tabSrc) { $(this).show().siblings('.J_iframe').hide(); return false; } }); } tabExist = true; return false; } }); if (tabExist) { return false; } // 3、Tab不存在,初始化新Tab + IFrame // build Tab (other tab no-active) $('.J_menuTab').removeClass('active'); var tabStr = '' + tabNameShow + ' '; // build IFrame (other ifame hide) var iframeStr = ''; // 4、添加Tab + IFrame // append iframe $('.J_mainContent').find('iframe.J_iframe').hide(); $('.J_mainContent').append(iframeStr); // append tab $('.J_menuTabs .page-tabs-content').append(tabStr); // 添加遮罩层 NProgress.inc(0.2); NProgress.configure({ easing: 'ease', // 动画缓动函数 (默认: 'ease') speed: 500, // 动画速度(毫秒)(默认: 200) showSpinner: true // 是否显示旋转图标 (默认: true) }); NProgress.start(); // load iframe let $iframe = $('.J_mainContent iframe:visible'); $iframe.on('load', function () { NProgress.done(); }).on('error', function () { NProgress.done(); // 处理加载失败情况,防止跳转 console.error('iframe load error, src = ' + $(this).attr('src')); }); // 5、滚动到已激活的Tab scrollToTab($('.J_menuTab.active')); return false; } /** * 关闭Tab:根据点击 Tab元素 * * @returns {boolean} */ function closeTab() { // param var closeTabId = $(this).parents('.J_menuTab').data('id'); var currentWidth = $(this).parents('.J_menuTab').width(); // process if ($(this).parents('.J_menuTab').hasClass('active')) { // 1、当前元素处于活动状态 // 当前元素后面有同辈元素,使后面的一个元素处于活动状态 if ($(this).parents('.J_menuTab').next('.J_menuTab').length > 0) { // 后一个Tab,激活 Tab var activeId = $(this).parents('.J_menuTab').next('.J_menuTab:eq(0)').data('id'); $(this).parents('.J_menuTab').next('.J_menuTab:eq(0)').addClass('active'); // 后一个Tab,激活 iframe $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') === activeId) { $(this).show().siblings('.J_iframe').hide(); return false; } }); var marginLeftVal = parseInt($('.page-tabs-content').css('margin-left')); if (marginLeftVal < 0) { $('.page-tabs-content').animate({ marginLeft: (marginLeftVal + currentWidth) + 'px' }, "fast"); } // 移除 当前 Tab $(this).parents('.J_menuTab').remove(); // 移除 当前 iframe $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') === closeTabId) { $(this).remove(); return false; } }); } // 当前元素后面没有同辈元素,使当前元素的上一个元素处于活动状态 if ($(this).parents('.J_menuTab').prev('.J_menuTab').length > 0) { // 前一个Tab,激活 Tab var activeId = $(this).parents('.J_menuTab').prev('.J_menuTab:last').data('id'); $(this).parents('.J_menuTab').prev('.J_menuTab:last').addClass('active'); // 前一个Tab,激活 iframe $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') == activeId) { $(this).show().siblings('.J_iframe').hide(); return false; } }); // 移除 当前 Tab $(this).parents('.J_menuTab').remove(); // 移除 当前 iframe $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') === closeTabId) { $(this).remove(); return false; } }); } } else { // 2、当前元素不处于活动状态 // 移除 当前 Tab $(this).parents('.J_menuTab').remove(); // 移除 当前 iframe $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') == closeTabId) { $(this).remove(); return false; } }); // 滚动到active状态 Tab scrollToTab($('.J_menuTab.active')); } // 3、菜单Menu联动 + 页面锚点(hash参数)更新 activeMenuAndPath($('.page-tabs-content').find('.active').attr('data-id')); return false; } /** * 菜单Menu联动 + 页面锚点(hash参数)更新 */ function activeMenuAndPath(tabSrc) { // 页面锚点(hash参数)更新 window.location.hash = tabSrc; // 菜单Menu切换/active $(".sidebar-menu ul li, .sidebar-menu li").removeClass("active"); $('.J_menuItem').each(function () { if ($(this).attr('href') === tabSrc) { // 菜单Menu切换/active $(this).parents("li").addClass("active"); return true; } }) return false; } /** * Tab单击(切换/激活):展示Tab页面 + 左侧菜单active更新 */ function activeTab() { // 是否已激活 active,避免重复处理 if (!$(this).hasClass('active')) { // 待激活Tab信息 let tabSrc = $(this).data('id'); // Ifame 切换展示 $('.J_mainContent .J_iframe').each(function () { if ($(this).data('id') === tabSrc) { $(this).show().siblings('.J_iframe').hide(); return false; } }); // Tab 激活 $(this).addClass('active').siblings('.J_menuTab').removeClass('active'); scrollToTab(this); // 菜单Menu联动 activeMenuAndPath(tabSrc); } } // -------------------- tab: scroll -------------------- /** * 滚动到指定选项卡 */ function scrollToTab(element) { // 当前元素左侧宽度 var marginLeftVal = calSumWidth($(element).prevAll()), marginRightVal = calSumWidth($(element).nextAll()); // Tab外部区域宽度 var tabOuterWidth = calSumWidth($(".content-tabs").children().not(".J_menuTabs")); // 可视区域Tab宽度 var visibleWidth = $(".content-tabs").outerWidth(true) - tabOuterWidth; // 实际滚动宽度 var scrollVal = 0; if ($(".page-tabs-content").outerWidth() < visibleWidth) { scrollVal = 0; } else if (marginRightVal <= (visibleWidth - $(element).outerWidth(true) - $(element).next().outerWidth(true))) { if ((visibleWidth - $(element).next().outerWidth(true)) > marginRightVal) { scrollVal = marginLeftVal; var tabElement = element; while ((scrollVal - $(tabElement).outerWidth()) > ($(".page-tabs-content").outerWidth() - visibleWidth)) { scrollVal -= $(tabElement).prev().outerWidth(); tabElement = $(tabElement).prev(); } } } else if (marginLeftVal > (visibleWidth - $(element).outerWidth(true) - $(element).prev().outerWidth(true))) { scrollVal = marginLeftVal - $(element).prev().outerWidth(true); } // 滚动处理 $('.page-tabs-content').animate({ marginLeft: 0 - scrollVal + 'px' }, "fast"); } /** * 计算元素集合的总宽度 */ function calSumWidth(elements) { var width = 0; $(elements).each(function () { width += $(this).outerWidth(true); }); return width; } // -------------------- tab: scroll-left、scroll-right、 -------------------- /** * 选项卡-左移按扭:查看左侧隐藏的选项卡 */ function scrollTabLeft() { // 当前元素左侧宽度 var marginLeftVal = Math.abs(parseInt($('.page-tabs-content').css('margin-left'))); // Tab外部区域宽度 var tabOuterWidth = calSumWidth($(".content-tabs").children().not(".J_menuTabs")); // 可视区域tab宽度 var visibleWidth = $(".content-tabs").outerWidth(true) - tabOuterWidth; // 实际滚动宽度 var scrollVal = 0; if ($(".page-tabs-content").width() < visibleWidth) { return false; } else { var tabElement = $(".J_menuTab:first"); var offsetVal = 0; while ((offsetVal + $(tabElement).outerWidth(true)) <= marginLeftVal) {//找到离当前tab最近的元素 offsetVal += $(tabElement).outerWidth(true); tabElement = $(tabElement).next(); } offsetVal = 0; if (calSumWidth($(tabElement).prevAll()) > visibleWidth) { while ((offsetVal + $(tabElement).outerWidth(true)) < (visibleWidth) && tabElement.length > 0) { offsetVal += $(tabElement).outerWidth(true); tabElement = $(tabElement).prev(); } scrollVal = calSumWidth($(tabElement).prevAll()); } } // 滚动处理 $('.page-tabs-content').animate({ marginLeft: 0 - scrollVal + 'px' }, "fast"); } /** * 选项卡-右移按扭:查看右侧隐藏的选项卡 */ function scrollTabRight() { // 当前元素左侧宽度 var marginLeftVal = Math.abs(parseInt($('.page-tabs-content').css('margin-left'))); // 可视区域非tab宽度 var tabOuterWidth = calSumWidth($(".content-tabs").children().not(".J_menuTabs")); // 可视区域tab宽度 var visibleWidth = $(".content-tabs").outerWidth(true) - tabOuterWidth; // 实际滚动宽度 var scrollVal = 0; if ($(".page-tabs-content").width() < visibleWidth) { return false; } else { var tabElement = $(".J_menuTab:first"); var offsetVal = 0; while ((offsetVal + $(tabElement).outerWidth(true)) <= marginLeftVal) {//找到离当前tab最近的元素 offsetVal += $(tabElement).outerWidth(true); tabElement = $(tabElement).next(); } offsetVal = 0; while ((offsetVal + $(tabElement).outerWidth(true)) < (visibleWidth) && tabElement.length > 0) { offsetVal += $(tabElement).outerWidth(true); tabElement = $(tabElement).next(); } scrollVal = calSumWidth($(tabElement).prevAll()); if (scrollVal > 0) { $('.page-tabs-content').animate({ marginLeft: 0 - scrollVal + 'px' }, "fast"); } } } // -------------------- tab: refresh、close-other、close-current、close-all -------------------- /** * Tab-刷新按钮:刷新active的 Tab 页面 */ function refreshTab() { // 显示进度条 NProgress.inc(0.2); NProgress.configure({ easing: 'ease', // 动画缓动函数 (默认: 'ease') speed: 500, // 动画速度(毫秒)(默认: 200) showSpinner: true // 是否显示旋转图标 (默认: true) }); NProgress.start(); // 1、获取当前激活的 Tab var tabSrc = $('.page-tabs-content').find('.active').attr('data-id'); var target = $('.J_iframe[data-id="' + tabSrc + '"]'); var url = target.attr('src'); // 2、重新加载页面 // target.attr('src', url).ready(); target.attr('src', url).on('load', function () { NProgress.done(); // 3、菜单Menu联动 + 页面锚点(hash参数)更新 activeMenuAndPath($('.page-tabs-content').find('.active').attr('data-id')); }).on('error', function () { NProgress.done(); // 处理加载失败情况,防止跳转 console.error('iframe load error, src = ' + $(this).attr('src')); }); } /** * 选项卡-Tab双击:刷新菜单页面 */ /*$('.J_menuTabs').on('dblclick', '.J_menuTab', refreshTab);*/ /** * 滚动到已激活的选项卡 */ /*$('.J_tabShowActive').on('click', showActiveTab); function showActiveTab(){ scrollToTab($('.J_menuTab.active')); }*/ /** * 关闭其他选项卡 */ function closeOtherTabs(){ $('.page-tabs-content').children("[data-id]").not(":first").not(".active").each(function () { $('.J_iframe[data-id="' + $(this).data('id') + '"]').remove(); $(this).remove(); }); $('.page-tabs-content').css("margin-left", "0"); } /** * 关闭当前选项卡 */ function tabCloseCurrent() { $('.page-tabs-content').find('.active i').trigger("click"); } /** * 关闭全部选项卡 */ function tabCloseAll(){ // 保留 第一个 Tab,其他删除(Tab+iframe) $('.page-tabs-content').children("[data-id]").not(":first").each(function () { $('.J_iframe[data-id="' + $(this).data('id') + '"]').remove(); $(this).remove(); }); // 第一个 Tab 激活/展示(Tab+iframe) $('.page-tabs-content').children("[data-id]:first").each(function () { $('.J_iframe[data-id="' + $(this).data('id') + '"]').show(); $(this).addClass("active"); }); $('.page-tabs-content').css("margin-left", "0"); // 菜单Menu联动 + 页面锚点(hash参数)更新 activeMenuAndPath($('.page-tabs-content').find('.active').attr('data-id')); } })(jQuery); ================================================ FILE: xxl-job-admin/src/main/resources/static/biz/common/admin.table.js ================================================ /*! * Admin Table for XXL-BOOT * ================ * * 1、data_filter: * searchBtn: 搜索 * resetBtn: 重置 * 2、data_operation: * action: * - add: 新增 * - update: 更新 * - delete: 删除 * style: * - selectOnlyOne: 单选 * - selectAny: 多选 * 3、data_list * mainDataTable: 表格 * * @author xuxueli * @repository https://github.com/xuxueli/xxl-boot */ (function($) { $.extend({ adminTable: { table :null, options: {}, selectIds: function () { // get select rows let rows = this.table.bootstrapTable('getSelections'); // find select ids return (rows && rows.length > 0) ? rows.map(row => row.id) : []; }, selectRows: function () { // get select rows return this.table.bootstrapTable('getSelections'); }, initTable: function(options) { // parse param this.table = $(options.table); this.options = options; // init filter initSearch(this.table, options); initReset(options); // init table initAdminTable(this.table, options); }, initTreeTable: function(options) { // parse param this.table = $(options.table); // init filter initSearch(this.table, options); initReset(options); // init tree table initAdminTreeTable(this.table, options); }, initDelete: function(options) { initDeleteFun(this.table, options); }, initAdd: function(options) { initAddFun(options); }, initUpdate: function(options) { initUpdateFun(this.table, options); } } }); /** * init search */ function initSearch(table, options){ // search $('#data_filter .searchBtn').on('click', function(){ // searchHandler let searchHandler = options.searchHandler; if (typeof searchHandler === 'function') { searchHandler(); return; } // do search $(table).bootstrapTable('refresh'); }); } /** * init reset */ function initReset(options) { // reset $('#data_filter .resetBtn').on('click', function(){ // reset let resetHandler = options.resetHandler; if (typeof resetHandler === 'function') { // resetHandler resetHandler(); } else { // do reset // input $('#data_filter input[type="text"]').val(''); // select $('#data_filter select').each(function() { $(this).prop('selectedIndex', 0); }); // checkbox $('#data_filter input[type="checkbox"]').prop('checked', false); // radio $('#data_filter input[type="radio"]').prop('checked', false); } // do search $('#data_filter .searchBtn').click(); }); } /** * init admin table * * @param table * @param url * @param queryParams * @param columns */ function initAdminTable(table, options) { // parse param let url = options.url; let queryParams = options.queryParams; let columns = options.columns; // init table $(table).bootstrapTable({ url: url, method: "post", contentType: "application/x-www-form-urlencoded", queryParamsType: "limit", queryParams: queryParams, // bootstrapTable -> queryParams sidePagination: "server", // server side page responseHandler: function (result) { // custome if (options.responseHandler) { return options.responseHandler(result); } // valid if (result.code !== 200) { layer.msg(result.msg || (I18n.system_opt+I18n.system_fail)); return { total: 0, rows: [] } } // default return { "total": result.data.total, "rows": result.data.data }; }, columns: columns, clickToSelect: true, // 是否启用点击选中行 multipleSelectRow: true, // 启动多选行:点击 选择单行,Shift+点击 选择连续行, Commond+点击 非连续选择多行 sortable: false, // 是否启用排序 pagination: true, // 是否显示分页 pageNumber: 1, // 默认第一页 pageList: [10, 25, 50, 100] , // 可供选择的每页的行数(*) smartDisplay: false, // 当总记录数小于分页数,是否显示可选项 paginationParts: ['pageInfoShort', 'pageSize', 'pageList'], paginationPreText: '<<', // 跳转页面的 上一页按钮 paginationNextText: '>>', // 跳转页面的 下一页按钮 paginationLoop: false, // 是否循环翻页 showRefresh: true, // 显示刷新按钮 showColumns: true, // 显示/隐藏列 minimumCountColumns: 2, // 最少允许的列数 // onLoadSuccess: function(data) {} onAll: function(name, args) { // filter if (!(['check.bs.table', "uncheck.bs.table", "check-all.bs.table", "uncheck-all.bs.table", 'post-body.bs.table'].indexOf(name) > -1)) { return false; } var rows = $(table).bootstrapTable('getSelections'); var selectLen = rows.length; if (selectLen > 0) { $("#data_operation .selectAny").removeClass('disabled'); } else { $("#data_operation .selectAny").addClass('disabled'); } if (selectLen === 1) { $("#data_operation .selectOnlyOne").removeClass('disabled'); } else { $("#data_operation .selectOnlyOne").addClass('disabled'); } } }); // toolbar 样式调整; var toolbarElement = document.querySelector('.fixed-table-toolbar'); if (toolbarElement) { toolbarElement.classList.remove('fixed-table-toolbar'); } } function initAdminTreeTable(table, options) { // parse param let url = options.url; let queryParams = options.queryParams; let columns = options.columns; // table var mainDataTable = $("#data_list").bootstrapTable({ url: url, method: "post", contentType: "application/x-www-form-urlencoded", queryParams: queryParams, responseHandler: function(result) { if (result.code !== 200) { layer.open({ icon: '2', content: result.msg }); return ; } return result.data; }, treeEnable:true, idField: 'id', // 树形id parentIdField: 'parentId', // 父级字段 treeShowField: 'name', // 树形字段 onPostBody: function(data) { $("#data_list").treegrid({ treeColumn: 1, // 选择第几列作为树形字段 initialState: 'expanded', // 默认展开;expanded、collapsed expanderExpandedClass: 'fa fa-fw fa-minus-square-o', // 树形展开图标 expanderCollapsedClass: 'fa fa-fw fa-plus-square-o', // 树形折叠图标 onChange () { $("#data_list").bootstrapTable('resetView') // 树形表格重绘 } }) }, columns:columns, clickToSelect: true, // 是否启用点击选中行 multipleSelectRow: true, // 启动多选行:点击 选择单行,Shift+点击 选择连续行, Commond+点击 非连续选择多行 sortable: false, // 是否启用排序 showRefresh: true, // 显示刷新按钮 showColumns: true, // 显示/隐藏列 minimumCountColumns: 2, // 最少允许的列数 onAll: function(name, args) { // filter if (!(['check.bs.table', "uncheck.bs.table", "check-all.bs.table", "uncheck-all.bs.table"].indexOf(name) > -1)) { return false; } var rows = mainDataTable.bootstrapTable('getSelections'); var selectLen = rows.length; if (selectLen > 0) { $("#data_operation .selectAny").removeClass('disabled'); } else { $("#data_operation .selectAny").addClass('disabled'); } if (selectLen === 1) { $("#data_operation .selectOnlyOne").removeClass('disabled'); } else { $("#data_operation .selectOnlyOne").addClass('disabled'); } } }); // toolbar 样式调整; var toolbarElement = document.querySelector('.fixed-table-toolbar'); if (toolbarElement) { toolbarElement.classList.remove('fixed-table-toolbar'); } } /** * init delete */ function initDeleteFun(table, options) { // parse param let url = options.url; // delete $("#data_operation").on('click', '.delete',function() { // get select rows var rows = $(table).bootstrapTable('getSelections'); // find select ids const selectIds = (rows && rows.length > 0) ? rows.map(row => row.id) : []; if (selectIds.length <= 0) { layer.msg(I18n.system_please_choose + I18n.system_data); return; } // do delete 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 : url, data : { "ids" : selectIds }, dataType : "json", success : function(data){ if (data.code === 200) { layer.msg( I18n.system_opt_del + I18n.system_success ); // refresh table $('#data_filter .searchBtn').click(); } else { layer.msg( data.msg || I18n.system_opt_del + I18n.system_fail ); } }, error: function(xhr, status, error) { // Handle error console.log("Error: " + error); layer.open({ icon: '2', content: (I18n.system_opt_del + I18n.system_fail) }); } }); }); }); } /** * init add */ function initAddFun(options) { // parse param let url = options.url; let rules = options.rules; let messages = options.messages; let writeFormData = options.writeFormData; let readFormData = options.readFormData; // add $("#data_operation .add").click(function(){ // reset addModalValidate.resetForm(); $("#addModal .form")[0].reset(); $("#addModal .form .form-group").removeClass("has-error"); // write FormData if (typeof writeFormData === 'function') { writeFormData(); } // show $('#addModal').modal({backdrop: false, keyboard: false}).modal('show'); }); var addModalValidate = $("#addModal .form").validate({ errorElement : 'span', errorClass : 'help-block', focusInvalid : true, rules : rules, // jquery.validate -> rules messages : messages, // jquery.validate -> messages 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 $.post(url, readFormData(), function(data, status) { if (data.code === 200) { $('#addModal').modal('hide'); layer.msg( I18n.system_opt_add + I18n.system_success ); // refresh table $('#data_filter .searchBtn').click(); } else { layer.open({ title: I18n.system_tips , btn: [ I18n.system_ok ], content: (data.msg || I18n.system_opt_add + I18n.system_fail ), icon: '2' }); } }); } }); } /** * init update */ function initUpdateFun(table, options) { // parse param let url = options.url; let rules = options.rules; let messages = options.messages; let writeFormData = options.writeFormData; let readFormData = options.readFormData; // update $("#data_operation .update").click(function(){ // get select rows var rows = $(table).bootstrapTable('getSelections'); // find select row if (rows.length !== 1) { layer.msg(I18n.system_please_choose + I18n.system_one + I18n.system_data); return; } var row = rows[0]; // reset $("#updateModal .form")[0].reset(); $("#updateModal .form .form-group").removeClass("has-error"); updateModalValidate.resetForm(); // write FormData writeFormData(row); // 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); }, rules : rules, // jquery.validate -> rules messages : messages, // jquery.validate -> messages submitHandler : function(form) { // request var paramData = readFormData(); $.post(url, paramData, function(data, status) { if (data.code === 200) { $('#updateModal').modal('hide'); layer.msg( I18n.system_opt_edit + I18n.system_success ); // refresh table $('#data_filter .searchBtn').click(); } else { layer.open({ title: I18n.system_tips , btn: [ I18n.system_ok ], content: (data.msg || I18n.system_opt_edit + I18n.system_fail ), icon: '2' }); } }); } }); } })(jQuery); ================================================ FILE: xxl-job-admin/src/main/resources/static/biz/common/admin.util.js ================================================ /*! * Admin Util for XXL-BOOT * ================ * * 1、openTab: 打开新页面,兼容iframe和window.open * * @author xuxueli * @repository https://github.com/xuxueli/xxl-boot */ $(function(){ // ---------------------- openTab ---------------------- /** * 打开新页面,兼容iframe和window.open * * @param tabSrc tab访问地址 * @param tabName tab展示名称 * @param isCloseCurrent 是否关闭当前页 */ window.openTab = function (tabSrc, tabName, isCloseCurrent) { // open tab if (window.parent.$.adminTab) { window.parent.$.adminTab.openTab({ tabSrc: tabSrc, tabName: tabName }, isCloseCurrent) } else { if (isCloseCurrent) { window.open(tabSrc, '_self'); } else { window.open(tabSrc, '_blank'); } } } // ---------------------- isOpenWithTab ---------------------- /** * 是否在Tab中打开 */ window.isOpenWithTab = function () { return !!window.parent.$.adminTab; } }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js ================================================ // 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)}; }); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css ================================================ .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; } ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js ================================================ // 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); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css ================================================ /* 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; } ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js ================================================ // 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; }))); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js ================================================ // 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 "( anything )", where // 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: '"'} } }); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js ================================================ // 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 }); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js ================================================ // 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); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js ================================================ // 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'); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js ================================================ // 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") }); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js ================================================ // 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'); }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js ================================================ (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 = $("
", { id: "CronContainer", style: "display:none;width:300px;height:300px;" }); var mainDiv = $("
", { id: "CronGenMainDiv", style: "width:410px;height:420px;" }); var topMenu = $("
    ", { "class": "nav nav-tabs", id: "CronGenTabs" }); $('
  • ', { 'class': 'active' }).html($('')).appendTo(topMenu); $('
  • ').html($('分钟')).appendTo(topMenu); $('
  • ').html($('小时')).appendTo(topMenu); $('
  • ').html($('')).appendTo(topMenu); $('
  • ').html($('')).appendTo(topMenu); $('
  • ').html($('')).appendTo(topMenu); $('
  • ').html($('')).appendTo(topMenu); $(topMenu).appendTo(mainDiv); //create what's inside the tabs var container = $("
    ", { "class": "container-fluid", "style": "margin-top: 30px;margin-left: -14px;" }); var row = $("
    ", { "class": "row-fluid" }); var span12 = $("
    ", { "class": "span12" }); var tabContent = $("
    ", { "class": "tab-content", "style": "border:0px; margin-top:-20px;" }); //creating the secondsTab var secondsTab = $("
    ", { "class": "tab-pane active", id: "Secondly" }); var seconds1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "second"}).appendTo(seconds1); $(seconds1).append("每秒 允许的通配符[, - * /]"); $(seconds1).appendTo(secondsTab); var seconds2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "second"}).appendTo(seconds2); $(seconds2).append("周期 从"); $("",{type : "text", id : "secondStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds2); $(seconds2).append("-"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "second"}).appendTo(seconds3); $(seconds3).append("从"); $("",{type : "text", id : "secondStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds3); $(seconds3).append("秒开始,每"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "4", name : "second", id: "sencond_appoint"}).appendTo(seconds4); $(seconds4).append("指定"); $(seconds4).appendTo(secondsTab); $(secondsTab).append('
    00010203040506070809
    '); $(secondsTab).append('
    10111213141516171819
    '); $(secondsTab).append('
    20212223242526272829
    '); $(secondsTab).append('
    30313233343536373839
    '); $(secondsTab).append('
    40414243444546474849
    '); $(secondsTab).append('
    50515253545556575859
    '); $("",{type : "hidden", id : "secondHidden"}).appendTo(secondsTab); $(secondsTab).appendTo(tabContent); //creating the minutesTab var minutesTab = $("
    ", { "class": "tab-pane", id: "Minutes" }); var minutes1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "min"}).appendTo(minutes1); $(minutes1).append("每分钟 允许的通配符[, - * /]"); $(minutes1).appendTo(minutesTab); var minutes2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "min"}).appendTo(minutes2); $(minutes2).append("周期 从"); $("",{type : "text", id : "minStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes2); $(minutes2).append("-"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "min"}).appendTo(minutes3); $(minutes3).append("从"); $("",{type : "text", id : "minStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes3); $(minutes3).append("分钟开始,每"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "4", name : "min", id: "min_appoint"}).appendTo(minutes4); $(minutes4).append("指定"); $(minutes4).appendTo(minutesTab); $(minutesTab).append('
    00010203040506070809
    '); $(minutesTab).append('
    10111213141516171819
    '); $(minutesTab).append('
    20212223242526272829
    '); $(minutesTab).append('
    30313233343536373839
    '); $(minutesTab).append('
    40414243444546474849
    '); $(minutesTab).append('
    50515253545556575859
    '); $("",{type : "hidden", id : "minHidden"}).appendTo(minutesTab); $(minutesTab).appendTo(tabContent); //creating the hourlyTab var hourlyTab = $("
    ", { "class": "tab-pane", id: "Hourly" }); var hourly1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "hour"}).appendTo(hourly1); $(hourly1).append("每小时 允许的通配符[, - * /]"); $(hourly1).appendTo(hourlyTab); var hourly2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "hour"}).appendTo(hourly2); $(hourly2).append("周期 从"); $("",{type : "text", id : "hourStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly2); $(hourly2).append("-"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "hour"}).appendTo(hourly3); $(hourly3).append("从"); $("",{type : "text", id : "hourStart_1", value : "0", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly3); $(hourly3).append("小时开始,每"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "4", name : "hour", id: "hour_appoint"}).appendTo(hourly4); $(hourly4).append("指定"); $(hourly4).appendTo(hourlyTab); $(hourlyTab).append('
    000102030405
    '); $(hourlyTab).append('
    060708091011
    '); $(hourlyTab).append('
    121314151617
    '); $(hourlyTab).append('
    181920212223
    '); $("",{type : "hidden", id : "hourHidden"}).appendTo(hourlyTab); $(hourlyTab).appendTo(tabContent); //creating the dailyTab var dailyTab = $("
    ", { "class": "tab-pane", id: "Daily" }); var daily1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "day"}).appendTo(daily1); $(daily1).append("每天 允许的通配符[, - * / L W]"); $(daily1).appendTo(dailyTab); var daily5 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "day"}).appendTo(daily5); $(daily5).append("不指定"); $(daily5).appendTo(dailyTab); var daily2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "day"}).appendTo(daily2); $(daily2).append("周期 从"); $("",{type : "text", id : "dayStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily2); $(daily2).append("-"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "4", name : "day"}).appendTo(daily3); $(daily3).append("从"); $("",{type : "text", id : "dayStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily3); $(daily3).append("日开始,每"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "5", name : "day"}).appendTo(daily6); $(daily6).append("每月"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "6", name : "day"}).appendTo(daily7); $(daily7).append("本月最后一天"); $(daily7).appendTo(dailyTab); var daily4 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "7", name : "day", id: "day_appoint"}).appendTo(daily4); $(daily4).append("指定"); $(daily4).appendTo(dailyTab); $(dailyTab).append('
    01020304050607080910
    '); $(dailyTab).append('
    11121314151617181920
    '); $(dailyTab).append('
    21222324252627282930
    '); $(dailyTab).append('
    31
    '); $("",{type : "hidden", id : "dayHidden"}).appendTo(dailyTab); $(dailyTab).appendTo(tabContent); //creating the monthlyTab var monthlyTab = $("
    ", { "class": "tab-pane", id: "Monthly" }); var monthly1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "month"}).appendTo(monthly1); $(monthly1).append("每月 允许的通配符[, - * /]"); $(monthly1).appendTo(monthlyTab); var monthly2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "month"}).appendTo(monthly2); $(monthly2).append("不指定"); $(monthly2).appendTo(monthlyTab); var monthly3 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "month"}).appendTo(monthly3); $(monthly3).append("周期 从"); $("",{type : "text", id : "monthStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly3); $(monthly3).append("-"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "4", name : "month"}).appendTo(monthly4); $(monthly4).append("从"); $("",{type : "text", id : "monthStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly4); $(monthly4).append("月开始,每"); $("",{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 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "5", name : "month", id: "month_appoint"}).appendTo(monthly5); $(monthly5).append("指定"); $(monthly5).appendTo(monthlyTab); $(monthlyTab).append('
    010203040506
    '); $(monthlyTab).append('
    070809101112
    '); $("",{type : "hidden", id : "monthHidden"}).appendTo(monthlyTab); $(monthlyTab).appendTo(tabContent); //creating the weeklyTab var weeklyTab = $("
    ", { "class": "tab-pane", id: "Weekly" }); var weekly1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "week"}).appendTo(weekly1); $(weekly1).append("每周 允许的通配符[, - * / L #]"); $(weekly1).appendTo(weeklyTab); var weekly2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "week"}).appendTo(weekly2); $(weekly2).append("不指定"); $(weekly2).appendTo(weeklyTab); var weekly3 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "week"}).appendTo(weekly3); $(weekly3).append("周期 每周第"); $("",{type : "text", id : "weekStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); $(weekly3).append("天-第"); $("",{type : "text", id : "weekEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); $(weekly3).append("天"); $(weekly3).appendTo(weeklyTab); var weekly4 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "4", name : "week"}).appendTo(weekly4); $(weekly4).append("从第"); $("",{type : "text", id : "weekStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); $(weekly4).append("天开始,间隔"); $("",{type : "text", id : "weekEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); $(weekly4).append("天执行一次"); $(weekly4).appendTo(weeklyTab); var weekly5 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "5", name : "week"}).appendTo(weekly5); $(weekly5).append("本月最后一周的第"); $("",{type : "text", id : "weekStart_2", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly5); $(weekly5).append("天"); $(weekly5).appendTo(weeklyTab); var weekly6 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "6", name : "week", id: "week_appoint"}).appendTo(weekly6); $(weekly6).append("指定"); $(weekly6).appendTo(weeklyTab); $(weeklyTab).append('
    周日周一周二周三周四周五周六
    '); $("",{type : "hidden", id : "weekHidden"}).appendTo(weeklyTab); $(weeklyTab).appendTo(tabContent); //creating the yearlyTab var yearlyTab = $("
    ", { "class": "tab-pane", id: "Yearly" }); var yearly1 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "1", name : "year"}).appendTo(yearly1); $(yearly1).append("不指定 允许的通配符[, - * /] 非必填"); $(yearly1).appendTo(yearlyTab); var yearly3 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "2", name : "year"}).appendTo(yearly3); $(yearly3).append("每年"); $(yearly3).appendTo(yearlyTab); var yearly2 = $("
    ",{"class":"line"}); $("",{type : "radio", value : "3", name : "year"}).appendTo(yearly2); $(yearly2).append("周期从"); $("",{type : "text", id : "yearStart_0", value : "2016", style:"width:45px; height:20px;"}).appendTo(yearly2); $(yearly2).append("-"); $("",{type : "text", id : "yearEnd_0", value : "2017", style:"width:45px; height:20px;"}).appendTo(yearly2); $(yearly2).append("年"); $(yearly2).appendTo(yearlyTab); $("",{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 = '

    '; $(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 = $("
    ").addClass("input-group"); // Add an input var $i = $("", { type: 'text', placeholder: 'cron表达式...', name: 'cronGen_display' }).addClass("form-control").val($(that).val()); $i.appendTo($g); // Add the button var $b = $(""); // Put button inside span var $s = $("").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: '

    ', 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(""); }; 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 ""; }; 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.data.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); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js ================================================ (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 = $("
    ", { id: "CronContainer", style: "display:none;width:300px;height:300px;" }); var mainDiv = $("
    ", { id: "CronGenMainDiv", style: "width:410px;height:420px;" }); var topMenu = $("
      ", { "class": "nav nav-tabs", id: "CronGenTabs" }); $('
    • ', { 'class': 'active' }).html($('')).appendTo(topMenu); $('
    • ').html($('Minute')).appendTo(topMenu); $('
    • ').html($('Hour')).appendTo(topMenu); $('
    • ').html($('Day')).appendTo(topMenu); $('
    • ').html($('Month')).appendTo(topMenu); $('
    • ').html($('Week')).appendTo(topMenu); $('
    • ').html($('Year')).appendTo(topMenu); $(topMenu).appendTo(mainDiv); //create what's inside the tabs var container = $("
      ", { "class": "container-fluid", "style": "margin-top: 30px;margin-left: -14px;" }); var row = $("
      ", { "class": "row-fluid" }); var span12 = $("
      ", { "class": "span12" }); var tabContent = $("
      ", { "class": "tab-content", "style": "border:0px; margin-top:-20px;" }); //creating the secondsTab var secondsTab = $("
      ", { "class": "tab-pane active", id: "Secondly" }); var seconds1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "second"}).appendTo(seconds1); $(seconds1).append("Per second, allowed wildcard[, - * /]"); $(seconds1).appendTo(secondsTab); var seconds2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "second"}).appendTo(seconds2); $(seconds2).append("Cycle, from"); $("",{type : "text", id : "secondStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(seconds2); $(seconds2).append("-"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "second"}).appendTo(seconds3); $(seconds3).append("from"); $("",{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"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "4", name : "second", id: "sencond_appoint"}).appendTo(seconds4); $(seconds4).append("specify"); $(seconds4).appendTo(secondsTab); $(secondsTab).append('
      00010203040506070809
      '); $(secondsTab).append('
      10111213141516171819
      '); $(secondsTab).append('
      20212223242526272829
      '); $(secondsTab).append('
      30313233343536373839
      '); $(secondsTab).append('
      40414243444546474849
      '); $(secondsTab).append('
      50515253545556575859
      '); $("",{type : "hidden", id : "secondHidden"}).appendTo(secondsTab); $(secondsTab).appendTo(tabContent); //creating the minutesTab var minutesTab = $("
      ", { "class": "tab-pane", id: "Minutes" }); var minutes1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "min"}).appendTo(minutes1); $(minutes1).append("Per minute, allowed wildcard[, - * /]"); $(minutes1).appendTo(minutesTab); var minutes2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "min"}).appendTo(minutes2); $(minutes2).append("Cycle, from"); $("",{type : "text", id : "minStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(minutes2); $(minutes2).append("-"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "min"}).appendTo(minutes3); $(minutes3).append("from"); $("",{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"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "4", name : "min", id: "min_appoint"}).appendTo(minutes4); $(minutes4).append("specify"); $(minutes4).appendTo(minutesTab); $(minutesTab).append('
      00010203040506070809
      '); $(minutesTab).append('
      10111213141516171819
      '); $(minutesTab).append('
      20212223242526272829
      '); $(minutesTab).append('
      30313233343536373839
      '); $(minutesTab).append('
      40414243444546474849
      '); $(minutesTab).append('
      50515253545556575859
      '); $("",{type : "hidden", id : "minHidden"}).appendTo(minutesTab); $(minutesTab).appendTo(tabContent); //creating the hourlyTab var hourlyTab = $("
      ", { "class": "tab-pane", id: "Hourly" }); var hourly1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "hour"}).appendTo(hourly1); $(hourly1).append("Per hour, allowed wildcard[, - * /]"); $(hourly1).appendTo(hourlyTab); var hourly2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "hour"}).appendTo(hourly2); $(hourly2).append("Cycle, from"); $("",{type : "text", id : "hourStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(hourly2); $(hourly2).append("-"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "hour"}).appendTo(hourly3); $(hourly3).append("from"); $("",{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"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "4", name : "hour", id: "hour_appoint"}).appendTo(hourly4); $(hourly4).append("specify"); $(hourly4).appendTo(hourlyTab); $(hourlyTab).append('
      000102030405
      '); $(hourlyTab).append('
      060708091011
      '); $(hourlyTab).append('
      121314151617
      '); $(hourlyTab).append('
      181920212223
      '); $("",{type : "hidden", id : "hourHidden"}).appendTo(hourlyTab); $(hourlyTab).appendTo(tabContent); //creating the dailyTab var dailyTab = $("
      ", { "class": "tab-pane", id: "Daily" }); var daily1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "day"}).appendTo(daily1); $(daily1).append("Per day, allowed wildcard[, - * / L W]"); $(daily1).appendTo(dailyTab); var daily5 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "day"}).appendTo(daily5); $(daily5).append("not specify"); $(daily5).appendTo(dailyTab); var daily2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "day"}).appendTo(daily2); $(daily2).append("Cycle, from"); $("",{type : "text", id : "dayStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(daily2); $(daily2).append("-"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "4", name : "day"}).appendTo(daily3); $(daily3).append("from"); $("",{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"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "5", name : "day"}).appendTo(daily6); $(daily6).append("The most recent working day on the 1"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "6", name : "day"}).appendTo(daily7); $(daily7).append("The last day of the month"); $(daily7).appendTo(dailyTab); var daily4 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "7", name : "day", id: "day_appoint"}).appendTo(daily4); $(daily4).append("specify"); $(daily4).appendTo(dailyTab); $(dailyTab).append('
      01020304050607080910
      '); $(dailyTab).append('
      11121314151617181920
      '); $(dailyTab).append('
      21222324252627282930
      '); $(dailyTab).append('
      31
      '); $("",{type : "hidden", id : "dayHidden"}).appendTo(dailyTab); $(dailyTab).appendTo(tabContent); //creating the monthlyTab var monthlyTab = $("
      ", { "class": "tab-pane", id: "Monthly" }); var monthly1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "month"}).appendTo(monthly1); $(monthly1).append("Per month, allowed wildcard[, - * /]"); $(monthly1).appendTo(monthlyTab); var monthly2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "month"}).appendTo(monthly2); $(monthly2).append("not specify"); $(monthly2).appendTo(monthlyTab); var monthly3 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "month"}).appendTo(monthly3); $(monthly3).append("Cycle, from"); $("",{type : "text", id : "monthStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(monthly3); $(monthly3).append("-"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "4", name : "month"}).appendTo(monthly4); $(monthly4).append("Starting from "); $("",{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"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "5", name : "month", id: "month_appoint"}).appendTo(monthly5); $(monthly5).append("specify"); $(monthly5).appendTo(monthlyTab); $(monthlyTab).append('
      010203040506
      '); $(monthlyTab).append('
      070809101112
      '); $("",{type : "hidden", id : "monthHidden"}).appendTo(monthlyTab); $(monthlyTab).appendTo(tabContent); //creating the weeklyTab var weeklyTab = $("
      ", { "class": "tab-pane", id: "Weekly" }); var weekly1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "week"}).appendTo(weekly1); $(weekly1).append("Per week, allowed wildcard[, - * / L #]"); $(weekly1).appendTo(weeklyTab); var weekly2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "week"}).appendTo(weekly2); $(weekly2).append("not specify"); $(weekly2).appendTo(weeklyTab); var weekly3 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "week"}).appendTo(weekly3); $(weekly3).append("Cycle, from week"); $("",{type : "text", id : "weekStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3); $(weekly3).append("-"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "4", name : "week"}).appendTo(weekly4); $(weekly4).append("The"); $("",{type : "text", id : "weekStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4); $(weekly4).append("th week, once every "); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "5", name : "week"}).appendTo(weekly5); $(weekly5).append("Last week of the month"); $("",{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 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "6", name : "week", id: "week_appoint"}).appendTo(weekly6); $(weekly6).append("specify"); $(weekly6).appendTo(weeklyTab); $(weeklyTab).append('
      SUNMONTUEWEDTHUFRISAT
      '); $("",{type : "hidden", id : "weekHidden"}).appendTo(weeklyTab); $(weeklyTab).appendTo(tabContent); //creating the yearlyTab var yearlyTab = $("
      ", { "class": "tab-pane", id: "Yearly" }); var yearly1 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "1", name : "year"}).appendTo(yearly1); $(yearly1).append("not specify allowed wildcard[, - * /] not required"); $(yearly1).appendTo(yearlyTab); var yearly3 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "2", name : "year"}).appendTo(yearly3); $(yearly3).append("Per year"); $(yearly3).appendTo(yearlyTab); var yearly2 = $("
      ",{"class":"line"}); $("",{type : "radio", value : "3", name : "year"}).appendTo(yearly2); $(yearly2).append("Cycle, from "); $("",{type : "text", id : "yearStart_0", value : "2016", style:"width:45px; height:20px;"}).appendTo(yearly2); $(yearly2).append("-"); $("",{type : "text", id : "yearEnd_0", value : "2017", style:"width:45px; height:20px;"}).appendTo(yearly2); $(yearly2).append("year"); $(yearly2).appendTo(yearlyTab); $("",{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 = '

      '; $(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 = $("
      ").addClass("input-group"); // Add an input var $i = $("", { type: 'text', placeholder: 'cron expression...', name: 'cronGen_display' }).addClass("form-control").val($(that).val()); $i.appendTo($g); // Add the button var $b = $(""); // Put button inside span var $s = $("").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: '

      ', 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(""); }; 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 ""; }; 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.data.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); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/fullscreen/jquery.fullscreen.js ================================================ /** * modify base jQuery FullScreen, support IE */ (function(jQuery) { /** * Sets or gets the fullscreen state. * * @param {boolean=} state * True to enable fullscreen mode, false to disable it. If not * specified then the current fullscreen state is returned. * @return {boolean|Element|jQuery|null} * When querying the fullscreen state then the current fullscreen * element (or true if browser doesn't support it) is returned * when browser is currently in full screen mode. False is returned * if browser is not in full screen mode. Null is returned if * browser doesn't support fullscreen mode at all. When setting * the fullscreen state then the current jQuery selection is * returned for chaining. * @this {jQuery} */ function fullScreen(state) { var e, func, doc; // Do nothing when nothing was selected if (!this.length) return this; // We only use the first selected element because it doesn't make sense // to fullscreen multiple elements. e = (/** @type {Element} */ this[0]); // Find the real element and the document (Depends on whether the // document itself or a HTML element was selected) if (e.ownerDocument) { doc = e.ownerDocument; } else { doc = e; e = doc.documentElement; } // When no state was specified then return the current state. if (state == null) { // When fullscreen mode is not supported then return null if (!((/** @type {?Function} */ doc["exitFullscreen"]) || (/** @type {?Function} */ doc["webkitExitFullscreen"]) || (/** @type {?Function} */ doc["webkitCancelFullScreen"]) || (/** @type {?Function} */ doc["msExitFullscreen"]) || (/** @type {?Function} */ doc["mozCancelFullScreen"]))) { return null; } // Check fullscreen state state = !!doc["fullscreenElement"] || !!doc["msFullscreenElement"] || !!doc["webkitIsFullScreen"] || !!doc["mozFullScreen"]; if (!state) return state; // Return current fullscreen element or "true" if browser doesn't // support this return (/** @type {?Element} */ doc["fullscreenElement"]) || (/** @type {?Element} */ doc["webkitFullscreenElement"]) || (/** @type {?Element} */ doc["webkitCurrentFullScreenElement"]) || (/** @type {?Element} */ doc["msFullscreenElement"]) || (/** @type {?Element} */ doc["mozFullScreenElement"]) || state; } // When state was specified then enter or exit fullscreen mode. if (state) { // Enter fullscreen func = (/** @type {?Function} */ e["requestFullscreen"]) || (/** @type {?Function} */ e["webkitRequestFullscreen"]) || (/** @type {?Function} */ e["webkitRequestFullScreen"]) || (/** @type {?Function} */ e["msRequestFullscreen"]) || (/** @type {?Function} */ e["mozRequestFullScreen"]); if (func) { func.call(e); } return this; } else { // Exit fullscreen func = (/** @type {?Function} */ doc["exitFullscreen"]) || (/** @type {?Function} */ doc["webkitExitFullscreen"]) || (/** @type {?Function} */ doc["webkitCancelFullScreen"]) || (/** @type {?Function} */ doc["msExitFullscreen"]) || (/** @type {?Function} */ doc["mozCancelFullScreen"]); if (func) func.call(doc); return this; } } /** * Toggles the fullscreen mode. * * @return {!jQuery} * The jQuery selection for chaining. * @this {jQuery} */ function toggleFullScreen() { return (/** @type {!jQuery} */ fullScreen.call(this, !fullScreen.call(this))); } /** * Handles the browser-specific fullscreenchange event and triggers * a jquery event for it. * * @param {?Event} event * The fullscreenchange event. */ function fullScreenChangeHandler(event) { jQuery(document).trigger(new jQuery.Event("fullscreenchange")); } /** * Handles the browser-specific fullscreenerror event and triggers * a jquery event for it. * * @param {?Event} event * The fullscreenerror event. */ function fullScreenErrorHandler(event) { jQuery(document).trigger(new jQuery.Event("fullscreenerror")); } /** * Installs the fullscreenchange event handler. */ function installFullScreenHandlers() { var e, change, error; // Determine event name e = document; if (e["webkitCancelFullScreen"]) { change = "webkitfullscreenchange"; error = "webkitfullscreenerror"; } else if (e["msExitFullscreen"]) { change = "MSFullscreenChange"; error = "MSFullscreenError"; } else if (e["mozCancelFullScreen"]) { change = "mozfullscreenchange"; error = "mozfullscreenerror"; } else { change = "fullscreenchange"; error = "fullscreenerror"; } // Install the event handlers jQuery(document).bind(change, fullScreenChangeHandler); jQuery(document).bind(error, fullScreenErrorHandler); } jQuery.fn["fullScreen"] = fullScreen; jQuery.fn["toggleFullScreen"] = toggleFullScreen; installFullScreenHandlers(); })(jQuery); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/jquery-treegrid/jquery.treegrid.css ================================================ .treegrid-indent {width:16px; height: 16px; display: inline-block; position: relative;} .treegrid-expander {width:16px; height: 16px; display: inline-block; position: relative; cursor: pointer;} .treegrid-expander-expanded{background-image: url(./img/collapse.png); } .treegrid-expander-collapsed{background-image: url(./img/expand.png);} ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/layer/layer.js ================================================ /*! 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?'
      '+(f?r.title[0]:r.title)+"
      ":"";return r.zIndex=s,t([r.shade?'
      ':"",'
      '+(e&&2!=r.type?"":u)+'
      '+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
      '+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
      '+e+"
      "}():"")+(r.resize?'':"")+"
      "],u,i('
      ')),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='';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]+'',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;af&&(a=f),ou&&(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?'":function(){return''}(),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=''+t[0].title+"";i"+t[i].title+"
      ";return a}(),content:'
        '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
      • '+(t[0].content||"no content")+"
      • ";i'+(t[i].content||"no content")+"";return a}()+"
      ",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]'+(u[d].alt||
      '+(u.length>1?'':"")+'
      '+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
      ",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("当前图片地址异常
      是否继续查看下一张?",{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); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css ================================================ .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}} ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/nprogress/nprogress.css ================================================ /* Make clicks pass-through */ #nprogress { pointer-events: none; } #nprogress .bar { background: #29d; position: fixed; z-index: 1031; top: 0; left: 0; width: 100%; height: 2px; } /* Fancy blur effect */ #nprogress .peg { display: block; position: absolute; right: 0px; width: 100px; height: 100%; box-shadow: 0 0 10px #29d, 0 0 5px #29d; opacity: 1.0; -webkit-transform: rotate(3deg) translate(0px, -4px); -ms-transform: rotate(3deg) translate(0px, -4px); transform: rotate(3deg) translate(0px, -4px); } /* Remove these to get rid of the spinner */ #nprogress .spinner { display: block; position: fixed; z-index: 1031; top: 15px; right: 15px; } #nprogress .spinner-icon { width: 18px; height: 18px; box-sizing: border-box; border: solid 2px transparent; border-top-color: #29d; border-left-color: #29d; border-radius: 50%; -webkit-animation: nprogress-spinner 400ms linear infinite; animation: nprogress-spinner 400ms linear infinite; } .nprogress-custom-parent { overflow: hidden; position: relative; } .nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .bar { position: absolute; } @-webkit-keyframes nprogress-spinner { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/nprogress/nprogress.js ================================================ /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress * @license MIT */ ;(function(root, factory) { if (typeof define === 'function' && define.amd) { define(factory); } else if (typeof exports === 'object') { module.exports = factory(); } else { root.NProgress = factory(); } })(this, function() { var NProgress = {}; NProgress.version = '0.2.0'; var Settings = NProgress.settings = { minimum: 0.08, easing: 'ease', positionUsing: '', speed: 200, trickle: true, trickleRate: 0.02, trickleSpeed: 800, showSpinner: true, barSelector: '[role="bar"]', spinnerSelector: '[role="spinner"]', parent: 'body', template: '
      ' }; /** * Updates configuration. * * NProgress.configure({ * minimum: 0.1 * }); */ NProgress.configure = function(options) { var key, value; for (key in options) { value = options[key]; if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value; } return this; }; /** * Last number. */ NProgress.status = null; /** * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`. * * NProgress.set(0.4); * NProgress.set(1.0); */ NProgress.set = function(n) { var started = NProgress.isStarted(); n = clamp(n, Settings.minimum, 1); NProgress.status = (n === 1 ? null : n); var progress = NProgress.render(!started), bar = progress.querySelector(Settings.barSelector), speed = Settings.speed, ease = Settings.easing; progress.offsetWidth; /* Repaint */ queue(function(next) { // Set positionUsing if it hasn't already been set if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS(); // Add transition css(bar, barPositionCSS(n, speed, ease)); if (n === 1) { // Fade out css(progress, { transition: 'none', opacity: 1 }); progress.offsetWidth; /* Repaint */ setTimeout(function() { css(progress, { transition: 'all ' + speed + 'ms linear', opacity: 0 }); setTimeout(function() { NProgress.remove(); next(); }, speed); }, speed); } else { setTimeout(next, speed); } }); return this; }; NProgress.isStarted = function() { return typeof NProgress.status === 'number'; }; /** * Shows the progress bar. * This is the same as setting the status to 0%, except that it doesn't go backwards. * * NProgress.start(); * */ NProgress.start = function() { if (!NProgress.status) NProgress.set(0); var work = function() { setTimeout(function() { if (!NProgress.status) return; NProgress.trickle(); work(); }, Settings.trickleSpeed); }; if (Settings.trickle) work(); return this; }; /** * Hides the progress bar. * This is the *sort of* the same as setting the status to 100%, with the * difference being `done()` makes some placebo effect of some realistic motion. * * NProgress.done(); * * If `true` is passed, it will show the progress bar even if its hidden. * * NProgress.done(true); */ NProgress.done = function(force) { if (!force && !NProgress.status) return this; return NProgress.inc(0.3 + 0.5 * Math.random()).set(1); }; /** * Increments by a random amount. */ NProgress.inc = function(amount) { var n = NProgress.status; if (!n) { return NProgress.start(); } else { if (typeof amount !== 'number') { amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95); } n = clamp(n + amount, 0, 0.994); return NProgress.set(n); } }; NProgress.trickle = function() { return NProgress.inc(Math.random() * Settings.trickleRate); }; /** * Waits for all supplied jQuery promises and * increases the progress as the promises resolve. * * @param $promise jQUery Promise */ (function() { var initial = 0, current = 0; NProgress.promise = function($promise) { if (!$promise || $promise.state() === "resolved") { return this; } if (current === 0) { NProgress.start(); } initial++; current++; $promise.always(function() { current--; if (current === 0) { initial = 0; NProgress.done(); } else { NProgress.set((initial - current) / initial); } }); return this; }; })(); /** * (Internal) renders the progress bar markup based on the `template` * setting. */ NProgress.render = function(fromStart) { if (NProgress.isRendered()) return document.getElementById('nprogress'); addClass(document.documentElement, 'nprogress-busy'); var progress = document.createElement('div'); progress.id = 'nprogress'; progress.innerHTML = Settings.template; var bar = progress.querySelector(Settings.barSelector), perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0), parent = document.querySelector(Settings.parent), spinner; css(bar, { transition: 'all 0 linear', transform: 'translate3d(' + perc + '%,0,0)' }); if (!Settings.showSpinner) { spinner = progress.querySelector(Settings.spinnerSelector); spinner && removeElement(spinner); } if (parent != document.body) { addClass(parent, 'nprogress-custom-parent'); } parent.appendChild(progress); return progress; }; /** * Removes the element. Opposite of render(). */ NProgress.remove = function() { removeClass(document.documentElement, 'nprogress-busy'); removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent'); var progress = document.getElementById('nprogress'); progress && removeElement(progress); }; /** * Checks if the progress bar is rendered. */ NProgress.isRendered = function() { return !!document.getElementById('nprogress'); }; /** * Determine which positioning CSS rule to use. */ NProgress.getPositioningCSS = function() { // Sniff on document.body.style var bodyStyle = document.body.style; // Sniff prefixes var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' : ('MozTransform' in bodyStyle) ? 'Moz' : ('msTransform' in bodyStyle) ? 'ms' : ('OTransform' in bodyStyle) ? 'O' : ''; if (vendorPrefix + 'Perspective' in bodyStyle) { // Modern browsers with 3D support, e.g. Webkit, IE10 return 'translate3d'; } else if (vendorPrefix + 'Transform' in bodyStyle) { // Browsers without 3D support, e.g. IE9 return 'translate'; } else { // Browsers without translate() support, e.g. IE7-8 return 'margin'; } }; /** * Helpers */ function clamp(n, min, max) { if (n < min) return min; if (n > max) return max; return n; } /** * (Internal) converts a percentage (`0..1`) to a bar translateX * percentage (`-100%..0%`). */ function toBarPerc(n) { return (-1 + n) * 100; } /** * (Internal) returns the correct CSS for changing the bar's * position given an n percentage, and speed and ease from Settings */ function barPositionCSS(n, speed, ease) { var barCSS; if (Settings.positionUsing === 'translate3d') { barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' }; } else if (Settings.positionUsing === 'translate') { barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' }; } else { barCSS = { 'margin-left': toBarPerc(n)+'%' }; } barCSS.transition = 'all '+speed+'ms '+ease; return barCSS; } /** * (Internal) Queues a function to be executed. */ var queue = (function() { var pending = []; function next() { var fn = pending.shift(); if (fn) { fn(next); } } return function(fn) { pending.push(fn); if (pending.length == 1) next(); }; })(); /** * (Internal) Applies css properties to an element, similar to the jQuery * css method. * * While this helper does assist with vendor prefixed property names, it * does not perform any manipulation of values prior to setting styles. */ var css = (function() { var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ], cssProps = {}; function camelCase(string) { return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) { return letter.toUpperCase(); }); } function getVendorProp(name) { var style = document.body.style; if (name in style) return name; var i = cssPrefixes.length, capName = name.charAt(0).toUpperCase() + name.slice(1), vendorName; while (i--) { vendorName = cssPrefixes[i] + capName; if (vendorName in style) return vendorName; } return name; } function getStyleProp(name) { name = camelCase(name); return cssProps[name] || (cssProps[name] = getVendorProp(name)); } function applyCss(element, prop, value) { prop = getStyleProp(prop); element.style[prop] = value; } return function(element, properties) { var args = arguments, prop, value; if (args.length == 2) { for (prop in properties) { value = properties[prop]; if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value); } } else { applyCss(element, args[1], args[2]); } } })(); /** * (Internal) Determines if an element or space separated list of class names contains a class name. */ function hasClass(element, name) { var list = typeof element == 'string' ? element : classList(element); return list.indexOf(' ' + name + ' ') >= 0; } /** * (Internal) Adds a class to an element. */ function addClass(element, name) { var oldList = classList(element), newList = oldList + name; if (hasClass(oldList, name)) return; // Trim the opening space. element.className = newList.substring(1); } /** * (Internal) Removes a class from an element. */ function removeClass(element, name) { var oldList = classList(element), newList; if (!hasClass(element, name)) return; // Replace the class name. newList = oldList.replace(' ' + name + ' ', ' '); // Trim the opening and closing spaces. element.className = newList.substring(1, newList.length - 1); } /** * (Internal) Gets a space separated list of the class names on the element. * The list is wrapped with a single space on each end to facilitate finding * matches within the list. */ function classList(element) { return (' ' + (element.className || '') + ' ').replace(/\s+/gi, ' '); } /** * (Internal) Removes an element from the DOM. */ function removeElement(element) { element && element.parentNode && element.parentNode.removeChild(element); } return NProgress; }); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/zTree/css/metroStyle/metroStyle.css ================================================ /*------------------------------------- zTree Style version: 3.4 author: Hunter.z email: hunter.z@263.net website: http://code.google.com/p/jquerytree/ -------------------------------------*/ .ztree * {padding:0; margin:0; font-size:12px; font-family: Verdana, Arial, Helvetica, AppleGothic, sans-serif} .ztree {margin:0; padding:5px; color:#333} .ztree li{padding:0; margin:0; list-style:none; line-height:17px; text-align:left; white-space:nowrap; outline:0} .ztree li ul{ margin:0; padding:0 0 0 18px} .ztree li ul.line{ background:url(./img/line_conn.png) 0 0 repeat-y;} .ztree li a {padding-right:3px; margin:0; cursor:pointer; height:21px; color:#333; background-color: transparent; text-decoration:none; vertical-align:top; display: inline-block} .ztree li a:hover {text-decoration:underline} .ztree li a.curSelectedNode {padding-top:0px; background-color:#e5e5e5; color:black; height:21px; opacity:0.8;} .ztree li a.curSelectedNode_Edit {padding-top:0px; background-color:#e5e5e5; color:black; height:21px; border:1px #666 solid; opacity:0.8;} .ztree li a.tmpTargetNode_inner {padding-top:0px; background-color:#aaa; color:white; height:21px; border:1px #666 solid; opacity:0.8; filter:alpha(opacity=80)} .ztree li a.tmpTargetNode_prev {} .ztree li a.tmpTargetNode_next {} .ztree li a input.rename {height:14px; width:80px; padding:0; margin:0; font-size:12px; border:1px #585956 solid; *border:0px} .ztree li span {line-height:21px; margin-right:2px} .ztree li span.button {line-height:0; margin:0; padding: 0; width:21px; height:21px; display: inline-block; vertical-align:middle; border:0 none; cursor: pointer;outline:none; background-color:transparent; background-repeat:no-repeat; background-attachment: scroll; background-image:url("./img/metro.png"); *background-image:url("./img/metro.gif")} .ztree li span.button.chk {width:13px; height:13px; margin:0 2px; cursor: auto} .ztree li span.button.chk.checkbox_false_full {background-position: -5px -5px;} .ztree li span.button.chk.checkbox_false_full_focus {background-position: -5px -26px;} .ztree li span.button.chk.checkbox_false_part {background-position: -5px -48px;} .ztree li span.button.chk.checkbox_false_part_focus {background-position: -5px -68px;} .ztree li span.button.chk.checkbox_false_disable {background-position: -5px -89px;} .ztree li span.button.chk.checkbox_true_full {background-position: -26px -5px;} .ztree li span.button.chk.checkbox_true_full_focus {background-position: -26px -26px;} .ztree li span.button.chk.checkbox_true_part {background-position: -26px -48px;} .ztree li span.button.chk.checkbox_true_part_focus {background-position: -26px -68px;} .ztree li span.button.chk.checkbox_true_disable {background-position: -26px -89px;} .ztree li span.button.chk.radio_false_full {background-position: -47px -5px;} .ztree li span.button.chk.radio_false_full_focus {background-position: -47px -26px;} .ztree li span.button.chk.radio_false_part {background-position: -47px -47px;} .ztree li span.button.chk.radio_false_part_focus {background-position: -47px -68px;} .ztree li span.button.chk.radio_false_disable {background-position: -47px -89px;} .ztree li span.button.chk.radio_true_full {background-position: -68px -5px;} .ztree li span.button.chk.radio_true_full_focus {background-position: -68px -26px;} .ztree li span.button.chk.radio_true_part {background-position: -68px -47px;} .ztree li span.button.chk.radio_true_part_focus {background-position: -68px -68px;} .ztree li span.button.chk.radio_true_disable {background-position: -68px -89px;} .ztree li span.button.switch {width:21px; height:21px} .ztree li span.button.root_open{background-position:-105px -63px} .ztree li span.button.root_close{background-position:-126px -63px} .ztree li span.button.roots_open{background-position: -105px 0;} .ztree li span.button.roots_close{background-position: -126px 0;} .ztree li span.button.center_open{background-position: -105px -21px;} .ztree li span.button.center_close{background-position: -126px -21px;} .ztree li span.button.bottom_open{background-position: -105px -42px;} .ztree li span.button.bottom_close{background-position: -126px -42px;} .ztree li span.button.noline_open{background-position: -105px -84px;} .ztree li span.button.noline_close{background-position: -126px -84px;} .ztree li span.button.root_docu{ background:none;} .ztree li span.button.roots_docu{background-position: -84px 0;} .ztree li span.button.center_docu{background-position: -84px -21px;} .ztree li span.button.bottom_docu{background-position: -84px -42px;} .ztree li span.button.noline_docu{ background:none;} .ztree li span.button.ico_open{margin-right:2px; background-position: -147px -21px; vertical-align:top; *vertical-align:middle} .ztree li span.button.ico_close{margin-right:2px; margin-right:2px; background-position: -147px 0; vertical-align:top; *vertical-align:middle} .ztree li span.button.ico_docu{margin-right:2px; background-position: -147px -42px; vertical-align:top; *vertical-align:middle} .ztree li span.button.edit {margin-left:2px; margin-right: -1px; background-position: -189px -21px; vertical-align:top; *vertical-align:middle} .ztree li span.button.edit:hover { background-position: -168px -21px; } .ztree li span.button.remove {margin-left:2px; margin-right: -1px; background-position: -189px -42px; vertical-align:top; *vertical-align:middle} .ztree li span.button.remove:hover { background-position: -168px -42px; } .ztree li span.button.add {margin-left:2px; margin-right: -1px; background-position: -189px 0; vertical-align:top; *vertical-align:middle} .ztree li span.button.add:hover { background-position: -168px 0; } .ztree li span.button.ico_loading{margin-right:2px; background:url(./img/loading.gif) no-repeat scroll 0 0 transparent; vertical-align:top; *vertical-align:middle} ul.tmpTargetzTree {background-color:#FFE6B0; opacity:0.8; filter:alpha(opacity=80)} span.tmpzTreeMove_arrow {width:16px; height:21px; display: inline-block; padding:0; margin:2px 0 0 1px; border:0 none; position:absolute; background-color:transparent; background-repeat:no-repeat; background-attachment: scroll; background-position:-168px -84px; background-image:url("./img/metro.png"); *background-image:url("./img/metro.gif")} ul.ztree.zTreeDragUL {margin:0; padding:0; position:absolute; width:auto; height:auto;overflow:hidden; background-color:#cfcfcf; border:1px #00B83F dotted; opacity:0.8; filter:alpha(opacity=80)} .ztreeMask {z-index:10000; background-color:#cfcfcf; opacity:0.0; filter:alpha(opacity=0); position:absolute} ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/zTree/js/jquery.ztree.core.js ================================================ /* * JQuery zTree core * v3.5.48 * http://treejs.cn/ * * Copyright (c) 2010 Hunter.z * * Licensed same as jquery - MIT License * http://www.opensource.org/licenses/mit-license.php * * Date: 2020-11-21 */ (function ($) { var settings = {}, roots = {}, caches = {}, //default consts of core _consts = { className: { BUTTON: "button", LEVEL: "level", ICO_LOADING: "ico_loading", SWITCH: "switch", NAME: 'node_name' }, event: { NODECREATED: "ztree_nodeCreated", CLICK: "ztree_click", EXPAND: "ztree_expand", COLLAPSE: "ztree_collapse", ASYNC_SUCCESS: "ztree_async_success", ASYNC_ERROR: "ztree_async_error", REMOVE: "ztree_remove", SELECTED: "ztree_selected", UNSELECTED: "ztree_unselected" }, id: { A: "_a", ICON: "_ico", SPAN: "_span", SWITCH: "_switch", UL: "_ul" }, line: { ROOT: "root", ROOTS: "roots", CENTER: "center", BOTTOM: "bottom", NOLINE: "noline", LINE: "line" }, folder: { OPEN: "open", CLOSE: "close", DOCU: "docu" }, node: { CURSELECTED: "curSelectedNode" } }, //default setting of core _setting = { treeId: "", treeObj: null, view: { addDiyDom: null, autoCancelSelected: true, dblClickExpand: true, expandSpeed: "fast", fontCss: {}, nodeClasses: {}, nameIsHTML: false, selectedMulti: true, showIcon: true, showLine: true, showTitle: true, txtSelectedEnable: false }, data: { key: { isParent: "isParent", children: "children", name: "name", title: "", url: "url", icon: "icon" }, render: { name: null, title: null, }, simpleData: { enable: false, idKey: "id", pIdKey: "pId", rootPId: null }, keep: { parent: false, leaf: false } }, async: { enable: false, contentType: "application/x-www-form-urlencoded", type: "post", dataType: "text", headers: {}, xhrFields: {}, url: "", autoParam: [], otherParam: [], dataFilter: null }, callback: { beforeAsync: null, beforeClick: null, beforeDblClick: null, beforeRightClick: null, beforeMouseDown: null, beforeMouseUp: null, beforeExpand: null, beforeCollapse: null, beforeRemove: null, onAsyncError: null, onAsyncSuccess: null, onNodeCreated: null, onClick: null, onDblClick: null, onRightClick: null, onMouseDown: null, onMouseUp: null, onExpand: null, onCollapse: null, onRemove: null } }, //default root of core //zTree use root to save full data _initRoot = function (setting) { var r = data.getRoot(setting); if (!r) { r = {}; data.setRoot(setting, r); } data.nodeChildren(setting, r, []); r.expandTriggerFlag = false; r.curSelectedList = []; r.noSelection = true; r.createdNodes = []; r.zId = 0; r._ver = (new Date()).getTime(); }, //default cache of core _initCache = function (setting) { var c = data.getCache(setting); if (!c) { c = {}; data.setCache(setting, c); } c.nodes = []; c.doms = []; }, //default bindEvent of core _bindEvent = function (setting) { var o = setting.treeObj, c = consts.event; o.bind(c.NODECREATED, function (event, treeId, node) { tools.apply(setting.callback.onNodeCreated, [event, treeId, node]); }); o.bind(c.CLICK, function (event, srcEvent, treeId, node, clickFlag) { tools.apply(setting.callback.onClick, [srcEvent, treeId, node, clickFlag]); }); o.bind(c.EXPAND, function (event, treeId, node) { tools.apply(setting.callback.onExpand, [event, treeId, node]); }); o.bind(c.COLLAPSE, function (event, treeId, node) { tools.apply(setting.callback.onCollapse, [event, treeId, node]); }); o.bind(c.ASYNC_SUCCESS, function (event, treeId, node, msg) { tools.apply(setting.callback.onAsyncSuccess, [event, treeId, node, msg]); }); o.bind(c.ASYNC_ERROR, function (event, treeId, node, XMLHttpRequest, textStatus, errorThrown) { tools.apply(setting.callback.onAsyncError, [event, treeId, node, XMLHttpRequest, textStatus, errorThrown]); }); o.bind(c.REMOVE, function (event, treeId, treeNode) { tools.apply(setting.callback.onRemove, [event, treeId, treeNode]); }); o.bind(c.SELECTED, function (event, treeId, node) { tools.apply(setting.callback.onSelected, [treeId, node]); }); o.bind(c.UNSELECTED, function (event, treeId, node) { tools.apply(setting.callback.onUnSelected, [treeId, node]); }); }, _unbindEvent = function (setting) { var o = setting.treeObj, c = consts.event; o.unbind(c.NODECREATED) .unbind(c.CLICK) .unbind(c.EXPAND) .unbind(c.COLLAPSE) .unbind(c.ASYNC_SUCCESS) .unbind(c.ASYNC_ERROR) .unbind(c.REMOVE) .unbind(c.SELECTED) .unbind(c.UNSELECTED); }, //default event proxy of core _eventProxy = function (event) { var target = event.target, setting = data.getSetting(event.data.treeId), tId = "", node = null, nodeEventType = "", treeEventType = "", nodeEventCallback = null, treeEventCallback = null, tmp = null; if (tools.eqs(event.type, "mousedown")) { treeEventType = "mousedown"; } else if (tools.eqs(event.type, "mouseup")) { treeEventType = "mouseup"; } else if (tools.eqs(event.type, "contextmenu")) { treeEventType = "contextmenu"; } else if (tools.eqs(event.type, "click")) { if (tools.eqs(target.tagName, "span") && target.getAttribute("treeNode" + consts.id.SWITCH) !== null) { tId = tools.getNodeMainDom(target).id; nodeEventType = "switchNode"; } else { tmp = tools.getMDom(setting, target, [{tagName: "a", attrName: "treeNode" + consts.id.A}]); if (tmp) { tId = tools.getNodeMainDom(tmp).id; nodeEventType = "clickNode"; } } } else if (tools.eqs(event.type, "dblclick")) { treeEventType = "dblclick"; tmp = tools.getMDom(setting, target, [{tagName: "a", attrName: "treeNode" + consts.id.A}]); if (tmp) { tId = tools.getNodeMainDom(tmp).id; nodeEventType = "switchNode"; } } if (treeEventType.length > 0 && tId.length == 0) { tmp = tools.getMDom(setting, target, [{tagName: "a", attrName: "treeNode" + consts.id.A}]); if (tmp) { tId = tools.getNodeMainDom(tmp).id; } } // event to node if (tId.length > 0) { node = data.getNodeCache(setting, tId); switch (nodeEventType) { case "switchNode" : var isParent = data.nodeIsParent(setting, node); if (!isParent) { nodeEventType = ""; } else if (tools.eqs(event.type, "click") || (tools.eqs(event.type, "dblclick") && tools.apply(setting.view.dblClickExpand, [setting.treeId, node], setting.view.dblClickExpand))) { nodeEventCallback = handler.onSwitchNode; } else { nodeEventType = ""; } break; case "clickNode" : nodeEventCallback = handler.onClickNode; break; } } // event to zTree switch (treeEventType) { case "mousedown" : treeEventCallback = handler.onZTreeMousedown; break; case "mouseup" : treeEventCallback = handler.onZTreeMouseup; break; case "dblclick" : treeEventCallback = handler.onZTreeDblclick; break; case "contextmenu" : treeEventCallback = handler.onZTreeContextmenu; break; } var proxyResult = { stop: false, node: node, nodeEventType: nodeEventType, nodeEventCallback: nodeEventCallback, treeEventType: treeEventType, treeEventCallback: treeEventCallback }; return proxyResult }, //default init node of core _initNode = function (setting, level, n, parentNode, isFirstNode, isLastNode, openFlag) { if (!n) return; var r = data.getRoot(setting), children = data.nodeChildren(setting, n); n.level = level; n.tId = setting.treeId + "_" + (++r.zId); n.parentTId = parentNode ? parentNode.tId : null; n.open = (typeof n.open == "string") ? tools.eqs(n.open, "true") : !!n.open; var isParent = data.nodeIsParent(setting, n); if (tools.isArray(children)) { data.nodeIsParent(setting, n, true); n.zAsync = true; } else { isParent = data.nodeIsParent(setting, n, isParent); n.open = (isParent && !setting.async.enable) ? n.open : false; n.zAsync = !isParent; } n.isFirstNode = isFirstNode; n.isLastNode = isLastNode; n.getParentNode = function () { return data.getNodeCache(setting, n.parentTId); }; n.getPreNode = function () { return data.getPreNode(setting, n); }; n.getNextNode = function () { return data.getNextNode(setting, n); }; n.getIndex = function () { return data.getNodeIndex(setting, n); }; n.getPath = function () { return data.getNodePath(setting, n); }; n.isAjaxing = false; data.fixPIdKeyValue(setting, n); }, _init = { bind: [_bindEvent], unbind: [_unbindEvent], caches: [_initCache], nodes: [_initNode], proxys: [_eventProxy], roots: [_initRoot], beforeA: [], afterA: [], innerBeforeA: [], innerAfterA: [], zTreeTools: [] }, //method of operate data data = { addNodeCache: function (setting, node) { data.getCache(setting).nodes[data.getNodeCacheId(node.tId)] = node; }, getNodeCacheId: function (tId) { return tId.substring(tId.lastIndexOf("_") + 1); }, addAfterA: function (afterA) { _init.afterA.push(afterA); }, addBeforeA: function (beforeA) { _init.beforeA.push(beforeA); }, addInnerAfterA: function (innerAfterA) { _init.innerAfterA.push(innerAfterA); }, addInnerBeforeA: function (innerBeforeA) { _init.innerBeforeA.push(innerBeforeA); }, addInitBind: function (bindEvent) { _init.bind.push(bindEvent); }, addInitUnBind: function (unbindEvent) { _init.unbind.push(unbindEvent); }, addInitCache: function (initCache) { _init.caches.push(initCache); }, addInitNode: function (initNode) { _init.nodes.push(initNode); }, addInitProxy: function (initProxy, isFirst) { if (!!isFirst) { _init.proxys.splice(0, 0, initProxy); } else { _init.proxys.push(initProxy); } }, addInitRoot: function (initRoot) { _init.roots.push(initRoot); }, addNodesData: function (setting, parentNode, index, nodes) { var children = data.nodeChildren(setting, parentNode), params; if (!children) { children = data.nodeChildren(setting, parentNode, []); index = -1; } else if (index >= children.length) { index = -1; } if (children.length > 0 && index === 0) { children[0].isFirstNode = false; view.setNodeLineIcos(setting, children[0]); } else if (children.length > 0 && index < 0) { children[children.length - 1].isLastNode = false; view.setNodeLineIcos(setting, children[children.length - 1]); } data.nodeIsParent(setting, parentNode, true); if (index < 0) { data.nodeChildren(setting, parentNode, children.concat(nodes)); } else { params = [index, 0].concat(nodes); children.splice.apply(children, params); } }, addSelectedNode: function (setting, node) { var root = data.getRoot(setting); if (!data.isSelectedNode(setting, node)) { root.curSelectedList.push(node); } }, addCreatedNode: function (setting, node) { if (!!setting.callback.onNodeCreated || !!setting.view.addDiyDom) { var root = data.getRoot(setting); root.createdNodes.push(node); } }, addZTreeTools: function (zTreeTools) { _init.zTreeTools.push(zTreeTools); }, exSetting: function (s) { $.extend(true, _setting, s); }, fixPIdKeyValue: function (setting, node) { if (setting.data.simpleData.enable) { node[setting.data.simpleData.pIdKey] = node.parentTId ? node.getParentNode()[setting.data.simpleData.idKey] : setting.data.simpleData.rootPId; } }, getAfterA: function (setting, node, array) { for (var i = 0, j = _init.afterA.length; i < j; i++) { _init.afterA[i].apply(this, arguments); } }, getBeforeA: function (setting, node, array) { for (var i = 0, j = _init.beforeA.length; i < j; i++) { _init.beforeA[i].apply(this, arguments); } }, getInnerAfterA: function (setting, node, array) { for (var i = 0, j = _init.innerAfterA.length; i < j; i++) { _init.innerAfterA[i].apply(this, arguments); } }, getInnerBeforeA: function (setting, node, array) { for (var i = 0, j = _init.innerBeforeA.length; i < j; i++) { _init.innerBeforeA[i].apply(this, arguments); } }, getCache: function (setting) { return caches[setting.treeId]; }, getNodeIndex: function (setting, node) { if (!node) return null; var p = node.parentTId ? node.getParentNode() : data.getRoot(setting), children = data.nodeChildren(setting, p); for (var i = 0, l = children.length - 1; i <= l; i++) { if (children[i] === node) { return i; } } return -1; }, getNextNode: function (setting, node) { if (!node) return null; var p = node.parentTId ? node.getParentNode() : data.getRoot(setting), children = data.nodeChildren(setting, p); for (var i = 0, l = children.length - 1; i <= l; i++) { if (children[i] === node) { return (i == l ? null : children[i + 1]); } } return null; }, getNodeByParam: function (setting, nodes, key, value) { if (!nodes || !key) return null; for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; if (node[key] == value) { return nodes[i]; } var children = data.nodeChildren(setting, node); var tmp = data.getNodeByParam(setting, children, key, value); if (tmp) return tmp; } return null; }, getNodeCache: function (setting, tId) { if (!tId) return null; var n = caches[setting.treeId].nodes[data.getNodeCacheId(tId)]; return n ? n : null; }, getNodePath: function (setting, node) { if (!node) return null; var path; if (node.parentTId) { path = node.getParentNode().getPath(); } else { path = []; } if (path) { path.push(node); } return path; }, getNodes: function (setting) { return data.nodeChildren(setting, data.getRoot(setting)); }, getNodesByParam: function (setting, nodes, key, value) { if (!nodes || !key) return []; var result = []; for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; if (node[key] == value) { result.push(node); } var children = data.nodeChildren(setting, node); result = result.concat(data.getNodesByParam(setting, children, key, value)); } return result; }, getNodesByParamFuzzy: function (setting, nodes, key, value) { if (!nodes || !key) return []; var result = []; value = value.toLowerCase(); for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; if (typeof node[key] == "string" && nodes[i][key].toLowerCase().indexOf(value) > -1) { result.push(node); } var children = data.nodeChildren(setting, node); result = result.concat(data.getNodesByParamFuzzy(setting, children, key, value)); } return result; }, getNodesByFilter: function (setting, nodes, filter, isSingle, invokeParam) { if (!nodes) return (isSingle ? null : []); var result = isSingle ? null : []; for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; if (tools.apply(filter, [node, invokeParam], false)) { if (isSingle) { return node; } result.push(node); } var children = data.nodeChildren(setting, node); var tmpResult = data.getNodesByFilter(setting, children, filter, isSingle, invokeParam); if (isSingle && !!tmpResult) { return tmpResult; } result = isSingle ? tmpResult : result.concat(tmpResult); } return result; }, getPreNode: function (setting, node) { if (!node) return null; var p = node.parentTId ? node.getParentNode() : data.getRoot(setting), children = data.nodeChildren(setting, p); for (var i = 0, l = children.length; i < l; i++) { if (children[i] === node) { return (i == 0 ? null : children[i - 1]); } } return null; }, getRoot: function (setting) { return setting ? roots[setting.treeId] : null; }, getRoots: function () { return roots; }, getSetting: function (treeId) { return settings[treeId]; }, getSettings: function () { return settings; }, getZTreeTools: function (treeId) { var r = this.getRoot(this.getSetting(treeId)); return r ? r.treeTools : null; }, initCache: function (setting) { for (var i = 0, j = _init.caches.length; i < j; i++) { _init.caches[i].apply(this, arguments); } }, initNode: function (setting, level, node, parentNode, preNode, nextNode) { for (var i = 0, j = _init.nodes.length; i < j; i++) { _init.nodes[i].apply(this, arguments); } }, initRoot: function (setting) { for (var i = 0, j = _init.roots.length; i < j; i++) { _init.roots[i].apply(this, arguments); } }, isSelectedNode: function (setting, node) { var root = data.getRoot(setting); for (var i = 0, j = root.curSelectedList.length; i < j; i++) { if (node === root.curSelectedList[i]) return true; } return false; }, nodeChildren: function (setting, node, newChildren) { if (!node) { return null; } var key = setting.data.key.children; if (typeof newChildren !== 'undefined') { node[key] = newChildren; } return node[key]; }, nodeIsParent: function (setting, node, newIsParent) { if (!node) { return false; } var key = setting.data.key.isParent; if (typeof newIsParent !== 'undefined') { if (typeof newIsParent === "string") { newIsParent = tools.eqs(newIsParent, "true"); } newIsParent = !!newIsParent; node[key] = newIsParent; } else if (typeof node[key] == "string"){ node[key] = tools.eqs(node[key], "true"); } else { node[key] = !!node[key]; } return node[key]; }, nodeName: function (setting, node, newName) { var key = setting.data.key.name; if (typeof newName !== 'undefined') { node[key] = newName; } var rawName = "" + node[key]; if(typeof setting.data.render.name === 'function') { return setting.data.render.name.call(this,rawName,node); } return rawName; }, nodeTitle: function (setting, node) { var t = setting.data.key.title === "" ? setting.data.key.name : setting.data.key.title; var rawTitle = "" + node[t]; if(typeof setting.data.render.title === 'function') { return setting.data.render.title.call(this,rawTitle,node); } return rawTitle; }, removeNodeCache: function (setting, node) { var children = data.nodeChildren(setting, node); if (children) { for (var i = 0, l = children.length; i < l; i++) { data.removeNodeCache(setting, children[i]); } } data.getCache(setting).nodes[data.getNodeCacheId(node.tId)] = null; }, removeSelectedNode: function (setting, node) { var root = data.getRoot(setting); for (var i = 0, j = root.curSelectedList.length; i < j; i++) { if (node === root.curSelectedList[i] || !data.getNodeCache(setting, root.curSelectedList[i].tId)) { root.curSelectedList.splice(i, 1); setting.treeObj.trigger(consts.event.UNSELECTED, [setting.treeId, node]); i--; j--; } } }, setCache: function (setting, cache) { caches[setting.treeId] = cache; }, setRoot: function (setting, root) { roots[setting.treeId] = root; }, setZTreeTools: function (setting, zTreeTools) { for (var i = 0, j = _init.zTreeTools.length; i < j; i++) { _init.zTreeTools[i].apply(this, arguments); } }, transformToArrayFormat: function (setting, nodes) { if (!nodes) return []; var r = []; if (tools.isArray(nodes)) { for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; _do(node); } } else { _do(nodes); } return r; function _do(_node) { r.push(_node); var children = data.nodeChildren(setting, _node); if (children) { r = r.concat(data.transformToArrayFormat(setting, children)); } } }, transformTozTreeFormat: function (setting, sNodes) { var i, l, key = setting.data.simpleData.idKey, parentKey = setting.data.simpleData.pIdKey; if (!key || key == "" || !sNodes) return []; if (tools.isArray(sNodes)) { var r = []; var tmpMap = {}; for (i = 0, l = sNodes.length; i < l; i++) { tmpMap[sNodes[i][key]] = sNodes[i]; } for (i = 0, l = sNodes.length; i < l; i++) { var p = tmpMap[sNodes[i][parentKey]]; if (p && sNodes[i][key] != sNodes[i][parentKey]) { var children = data.nodeChildren(setting, p); if (!children) { children = data.nodeChildren(setting, p, []); } children.push(sNodes[i]); } else { r.push(sNodes[i]); } } return r; } else { return [sNodes]; } } }, //method of event proxy event = { bindEvent: function (setting) { for (var i = 0, j = _init.bind.length; i < j; i++) { _init.bind[i].apply(this, arguments); } }, unbindEvent: function (setting) { for (var i = 0, j = _init.unbind.length; i < j; i++) { _init.unbind[i].apply(this, arguments); } }, bindTree: function (setting) { var eventParam = { treeId: setting.treeId }, o = setting.treeObj; if (!setting.view.txtSelectedEnable) { // for can't select text o.bind('selectstart', handler.onSelectStart).css({ "-moz-user-select": "-moz-none" }); } o.bind('click', eventParam, event.proxy); o.bind('dblclick', eventParam, event.proxy); o.bind('mouseover', eventParam, event.proxy); o.bind('mouseout', eventParam, event.proxy); o.bind('mousedown', eventParam, event.proxy); o.bind('mouseup', eventParam, event.proxy); o.bind('contextmenu', eventParam, event.proxy); }, unbindTree: function (setting) { var o = setting.treeObj; o.unbind('selectstart', handler.onSelectStart) .unbind('click', event.proxy) .unbind('dblclick', event.proxy) .unbind('mouseover', event.proxy) .unbind('mouseout', event.proxy) .unbind('mousedown', event.proxy) .unbind('mouseup', event.proxy) .unbind('contextmenu', event.proxy); }, doProxy: function (e) { var results = []; for (var i = 0, j = _init.proxys.length; i < j; i++) { var proxyResult = _init.proxys[i].apply(this, arguments); results.push(proxyResult); if (proxyResult.stop) { break; } } return results; }, proxy: function (e) { var setting = data.getSetting(e.data.treeId); if (!tools.uCanDo(setting, e)) return true; var results = event.doProxy(e), r = true, x = false; for (var i = 0, l = results.length; i < l; i++) { var proxyResult = results[i]; if (proxyResult.nodeEventCallback) { x = true; r = proxyResult.nodeEventCallback.apply(proxyResult, [e, proxyResult.node]) && r; } if (proxyResult.treeEventCallback) { x = true; r = proxyResult.treeEventCallback.apply(proxyResult, [e, proxyResult.node]) && r; } } return r; } }, //method of event handler handler = { onSwitchNode: function (event, node) { var setting = data.getSetting(event.data.treeId); if (node.open) { if (tools.apply(setting.callback.beforeCollapse, [setting.treeId, node], true) == false) return true; data.getRoot(setting).expandTriggerFlag = true; view.switchNode(setting, node); } else { if (tools.apply(setting.callback.beforeExpand, [setting.treeId, node], true) == false) return true; data.getRoot(setting).expandTriggerFlag = true; view.switchNode(setting, node); } return true; }, onClickNode: function (event, node) { var setting = data.getSetting(event.data.treeId), clickFlag = ((setting.view.autoCancelSelected && (event.ctrlKey || event.metaKey)) && data.isSelectedNode(setting, node)) ? 0 : (setting.view.autoCancelSelected && (event.ctrlKey || event.metaKey) && setting.view.selectedMulti) ? 2 : 1; if (tools.apply(setting.callback.beforeClick, [setting.treeId, node, clickFlag], true) == false) return true; if (clickFlag === 0) { view.cancelPreSelectedNode(setting, node); } else { view.selectNode(setting, node, clickFlag === 2); } setting.treeObj.trigger(consts.event.CLICK, [event, setting.treeId, node, clickFlag]); return true; }, onZTreeMousedown: function (event, node) { var setting = data.getSetting(event.data.treeId); if (tools.apply(setting.callback.beforeMouseDown, [setting.treeId, node], true)) { tools.apply(setting.callback.onMouseDown, [event, setting.treeId, node]); } return true; }, onZTreeMouseup: function (event, node) { var setting = data.getSetting(event.data.treeId); if (tools.apply(setting.callback.beforeMouseUp, [setting.treeId, node], true)) { tools.apply(setting.callback.onMouseUp, [event, setting.treeId, node]); } return true; }, onZTreeDblclick: function (event, node) { var setting = data.getSetting(event.data.treeId); if (tools.apply(setting.callback.beforeDblClick, [setting.treeId, node], true)) { tools.apply(setting.callback.onDblClick, [event, setting.treeId, node]); } return true; }, onZTreeContextmenu: function (event, node) { var setting = data.getSetting(event.data.treeId); if (tools.apply(setting.callback.beforeRightClick, [setting.treeId, node], true)) { tools.apply(setting.callback.onRightClick, [event, setting.treeId, node]); } return (typeof setting.callback.onRightClick) != "function"; }, onSelectStart: function (e) { var n = e.originalEvent.srcElement.nodeName.toLowerCase(); return (n === "input" || n === "textarea"); } }, //method of tools for zTree tools = { apply: function (fun, param, defaultValue) { if ((typeof fun) == "function") { return fun.apply(zt, param ? param : []); } return defaultValue; }, canAsync: function (setting, node) { var children = data.nodeChildren(setting, node); var isParent = data.nodeIsParent(setting, node); return (setting.async.enable && node && isParent && !(node.zAsync || (children && children.length > 0))); }, clone: function (obj) { if (obj === null) return null; var o = tools.isArray(obj) ? [] : {}; for (var i in obj) { o[i] = (obj[i] instanceof Date) ? new Date(obj[i].getTime()) : (typeof obj[i] === "object" ? tools.clone(obj[i]) : obj[i]); } return o; }, eqs: function (str1, str2) { return str1.toLowerCase() === str2.toLowerCase(); }, isArray: function (arr) { return Object.prototype.toString.apply(arr) === "[object Array]"; }, isElement: function (o) { return ( typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2 o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string" ); }, $: function (node, exp, setting) { if (!!exp && typeof exp != "string") { setting = exp; exp = ""; } if (typeof node == "string") { return $(node, setting ? setting.treeObj.get(0).ownerDocument : null); } else { return $("#" + node.tId + exp, setting ? setting.treeObj : null); } }, getMDom: function (setting, curDom, targetExpr) { if (!curDom) return null; while (curDom && curDom.id !== setting.treeId) { for (var i = 0, l = targetExpr.length; curDom.tagName && i < l; i++) { if (tools.eqs(curDom.tagName, targetExpr[i].tagName) && curDom.getAttribute(targetExpr[i].attrName) !== null) { return curDom; } } curDom = curDom.parentNode; } return null; }, getNodeMainDom: function (target) { return ($(target).parent("li").get(0) || $(target).parentsUntil("li").parent().get(0)); }, isChildOrSelf: function (dom, parentId) { return ($(dom).closest("#" + parentId).length > 0); }, uCanDo: function (setting, e) { return true; } }, //method of operate ztree dom view = { addNodes: function (setting, parentNode, index, newNodes, isSilent) { var isParent = data.nodeIsParent(setting, parentNode); if (setting.data.keep.leaf && parentNode && !isParent) { return; } if (!tools.isArray(newNodes)) { newNodes = [newNodes]; } if (setting.data.simpleData.enable) { newNodes = data.transformTozTreeFormat(setting, newNodes); } if (parentNode) { var target_switchObj = $$(parentNode, consts.id.SWITCH, setting), target_icoObj = $$(parentNode, consts.id.ICON, setting), target_ulObj = $$(parentNode, consts.id.UL, setting); if (!parentNode.open) { view.replaceSwitchClass(parentNode, target_switchObj, consts.folder.CLOSE); view.replaceIcoClass(parentNode, target_icoObj, consts.folder.CLOSE); parentNode.open = false; target_ulObj.css({ "display": "none" }); } data.addNodesData(setting, parentNode, index, newNodes); view.createNodes(setting, parentNode.level + 1, newNodes, parentNode, index); if (!isSilent) { view.expandCollapseParentNode(setting, parentNode, true); } } else { data.addNodesData(setting, data.getRoot(setting), index, newNodes); view.createNodes(setting, 0, newNodes, null, index); } }, appendNodes: function (setting, level, nodes, parentNode, index, initFlag, openFlag) { if (!nodes) return []; var html = []; var tmpPNode = (parentNode) ? parentNode : data.getRoot(setting), tmpPChild = data.nodeChildren(setting, tmpPNode), isFirstNode, isLastNode; if (!tmpPChild || index >= tmpPChild.length - nodes.length) { index = -1; } for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; if (initFlag) { isFirstNode = ((index === 0 || tmpPChild.length == nodes.length) && (i == 0)); isLastNode = (index < 0 && i == (nodes.length - 1)); data.initNode(setting, level, node, parentNode, isFirstNode, isLastNode, openFlag); data.addNodeCache(setting, node); } var isParent = data.nodeIsParent(setting, node); var childHtml = []; var children = data.nodeChildren(setting, node); if (children && children.length > 0) { //make child html first, because checkType childHtml = view.appendNodes(setting, level + 1, children, node, -1, initFlag, openFlag && node.open); } if (openFlag) { view.makeDOMNodeMainBefore(html, setting, node); view.makeDOMNodeLine(html, setting, node); data.getBeforeA(setting, node, html); view.makeDOMNodeNameBefore(html, setting, node); data.getInnerBeforeA(setting, node, html); view.makeDOMNodeIcon(html, setting, node); data.getInnerAfterA(setting, node, html); view.makeDOMNodeNameAfter(html, setting, node); data.getAfterA(setting, node, html); if (isParent && node.open) { view.makeUlHtml(setting, node, html, childHtml.join('')); } view.makeDOMNodeMainAfter(html, setting, node); data.addCreatedNode(setting, node); } } return html; }, appendParentULDom: function (setting, node) { var html = [], nObj = $$(node, setting); if (!nObj.get(0) && !!node.parentTId) { view.appendParentULDom(setting, node.getParentNode()); nObj = $$(node, setting); } var ulObj = $$(node, consts.id.UL, setting); if (ulObj.get(0)) { ulObj.remove(); } var children = data.nodeChildren(setting, node), childHtml = view.appendNodes(setting, node.level + 1, children, node, -1, false, true); view.makeUlHtml(setting, node, html, childHtml.join('')); nObj.append(html.join('')); }, asyncNode: function (setting, node, isSilent, callback) { var i, l; var isParent = data.nodeIsParent(setting, node); if (node && !isParent) { tools.apply(callback); return false; } else if (node && node.isAjaxing) { return false; } else if (tools.apply(setting.callback.beforeAsync, [setting.treeId, node], true) == false) { tools.apply(callback); return false; } if (node) { node.isAjaxing = true; var icoObj = $$(node, consts.id.ICON, setting); icoObj.attr({"style": "", "class": consts.className.BUTTON + " " + consts.className.ICO_LOADING}); } var tmpParam = {}; var autoParam = tools.apply(setting.async.autoParam, [setting.treeId, node], setting.async.autoParam); for (i = 0, l = autoParam.length; node && i < l; i++) { var pKey = autoParam[i].split("="), spKey = pKey; if (pKey.length > 1) { spKey = pKey[1]; pKey = pKey[0]; } tmpParam[spKey] = node[pKey]; } var otherParam = tools.apply(setting.async.otherParam, [setting.treeId, node], setting.async.otherParam); if (tools.isArray(otherParam)) { for (i = 0, l = otherParam.length; i < l; i += 2) { tmpParam[otherParam[i]] = otherParam[i + 1]; } } else { for (var p in otherParam) { tmpParam[p] = otherParam[p]; } } var _tmpV = data.getRoot(setting)._ver; $.ajax({ contentType: setting.async.contentType, cache: false, type: setting.async.type, url: tools.apply(setting.async.url, [setting.treeId, node], setting.async.url), data: setting.async.contentType.indexOf('application/json') > -1 ? JSON.stringify(tmpParam) : tmpParam, dataType: setting.async.dataType, headers: setting.async.headers, xhrFields: setting.async.xhrFields, success: function (msg) { if (_tmpV != data.getRoot(setting)._ver) { return; } var newNodes = []; try { if (!msg || msg.length == 0) { newNodes = []; } else if (typeof msg == "string") { newNodes = eval("(" + msg + ")"); } else { newNodes = msg; } } catch (err) { newNodes = msg; } if (node) { node.isAjaxing = null; node.zAsync = true; } view.setNodeLineIcos(setting, node); if (newNodes && newNodes !== "") { newNodes = tools.apply(setting.async.dataFilter, [setting.treeId, node, newNodes], newNodes); view.addNodes(setting, node, -1, !!newNodes ? tools.clone(newNodes) : [], !!isSilent); } else { view.addNodes(setting, node, -1, [], !!isSilent); } setting.treeObj.trigger(consts.event.ASYNC_SUCCESS, [setting.treeId, node, msg]); tools.apply(callback); }, error: function (XMLHttpRequest, textStatus, errorThrown) { if (_tmpV != data.getRoot(setting)._ver) { return; } if (node) node.isAjaxing = null; view.setNodeLineIcos(setting, node); setting.treeObj.trigger(consts.event.ASYNC_ERROR, [setting.treeId, node, XMLHttpRequest, textStatus, errorThrown]); } }); return true; }, cancelPreSelectedNode: function (setting, node, excludeNode) { var list = data.getRoot(setting).curSelectedList, i, n; for (i = list.length - 1; i >= 0; i--) { n = list[i]; if (node === n || (!node && (!excludeNode || excludeNode !== n))) { $$(n, consts.id.A, setting).removeClass(consts.node.CURSELECTED); if (node) { data.removeSelectedNode(setting, node); break; } else { list.splice(i, 1); setting.treeObj.trigger(consts.event.UNSELECTED, [setting.treeId, n]); } } } }, createNodeCallback: function (setting) { if (!!setting.callback.onNodeCreated || !!setting.view.addDiyDom) { var root = data.getRoot(setting); while (root.createdNodes.length > 0) { var node = root.createdNodes.shift(); tools.apply(setting.view.addDiyDom, [setting.treeId, node]); if (!!setting.callback.onNodeCreated) { setting.treeObj.trigger(consts.event.NODECREATED, [setting.treeId, node]); } } } }, createNodes: function (setting, level, nodes, parentNode, index) { if (!nodes || nodes.length == 0) return; var root = data.getRoot(setting), openFlag = !parentNode || parentNode.open || !!$$(data.nodeChildren(setting, parentNode)[0], setting).get(0); root.createdNodes = []; var zTreeHtml = view.appendNodes(setting, level, nodes, parentNode, index, true, openFlag), parentObj, nextObj; if (!parentNode) { parentObj = setting.treeObj; //setting.treeObj.append(zTreeHtml.join('')); } else { var ulObj = $$(parentNode, consts.id.UL, setting); if (ulObj.get(0)) { parentObj = ulObj; //ulObj.append(zTreeHtml.join('')); } } if (parentObj) { if (index >= 0) { nextObj = parentObj.children()[index]; } if (index >= 0 && nextObj) { $(nextObj).before(zTreeHtml.join('')); } else { parentObj.append(zTreeHtml.join('')); } } view.createNodeCallback(setting); }, destroy: function (setting) { if (!setting) return; data.initCache(setting); data.initRoot(setting); event.unbindTree(setting); event.unbindEvent(setting); setting.treeObj.empty(); delete settings[setting.treeId]; }, expandCollapseNode: function (setting, node, expandFlag, animateFlag, callback) { var root = data.getRoot(setting); var tmpCb, _callback; if (!node) { tools.apply(callback, []); return; } var children = data.nodeChildren(setting, node); var isParent = data.nodeIsParent(setting, node); if (root.expandTriggerFlag) { _callback = callback; tmpCb = function () { if (_callback) _callback(); if (node.open) { setting.treeObj.trigger(consts.event.EXPAND, [setting.treeId, node]); } else { setting.treeObj.trigger(consts.event.COLLAPSE, [setting.treeId, node]); } }; callback = tmpCb; root.expandTriggerFlag = false; } if (!node.open && isParent && ((!$$(node, consts.id.UL, setting).get(0)) || (children && children.length > 0 && !$$(children[0], setting).get(0)))) { view.appendParentULDom(setting, node); view.createNodeCallback(setting); } if (node.open == expandFlag) { tools.apply(callback, []); return; } var ulObj = $$(node, consts.id.UL, setting), switchObj = $$(node, consts.id.SWITCH, setting), icoObj = $$(node, consts.id.ICON, setting); if (isParent) { node.open = !node.open; if (node.iconOpen && node.iconClose) { icoObj.attr("style", view.makeNodeIcoStyle(setting, node)); } if (node.open) { view.replaceSwitchClass(node, switchObj, consts.folder.OPEN); view.replaceIcoClass(node, icoObj, consts.folder.OPEN); if (animateFlag == false || setting.view.expandSpeed == "") { ulObj.show(); tools.apply(callback, []); } else { if (children && children.length > 0) { ulObj.slideDown(setting.view.expandSpeed, callback); } else { ulObj.show(); tools.apply(callback, []); } } } else { view.replaceSwitchClass(node, switchObj, consts.folder.CLOSE); view.replaceIcoClass(node, icoObj, consts.folder.CLOSE); if (animateFlag == false || setting.view.expandSpeed == "" || !(children && children.length > 0)) { ulObj.hide(); tools.apply(callback, []); } else { ulObj.slideUp(setting.view.expandSpeed, callback); } } } else { tools.apply(callback, []); } }, expandCollapseParentNode: function (setting, node, expandFlag, animateFlag, callback) { if (!node) return; if (!node.parentTId) { view.expandCollapseNode(setting, node, expandFlag, animateFlag, callback); return; } else { view.expandCollapseNode(setting, node, expandFlag, animateFlag); } if (node.parentTId) { view.expandCollapseParentNode(setting, node.getParentNode(), expandFlag, animateFlag, callback); } }, expandCollapseSonNode: function (setting, node, expandFlag, animateFlag, callback) { var root = data.getRoot(setting), treeNodes = (node) ? data.nodeChildren(setting, node) : data.nodeChildren(setting, root), selfAnimateSign = (node) ? false : animateFlag, expandTriggerFlag = data.getRoot(setting).expandTriggerFlag; data.getRoot(setting).expandTriggerFlag = false; if (treeNodes) { for (var i = 0, l = treeNodes.length; i < l; i++) { if (treeNodes[i]) view.expandCollapseSonNode(setting, treeNodes[i], expandFlag, selfAnimateSign); } } data.getRoot(setting).expandTriggerFlag = expandTriggerFlag; view.expandCollapseNode(setting, node, expandFlag, animateFlag, callback); }, isSelectedNode: function (setting, node) { if (!node) { return false; } var list = data.getRoot(setting).curSelectedList, i; for (i = list.length - 1; i >= 0; i--) { if (node === list[i]) { return true; } } return false; }, makeDOMNodeIcon: function (html, setting, node) { var nameStr = data.nodeName(setting, node), name = setting.view.nameIsHTML ? nameStr : nameStr.replace(/&/g, '&').replace(//g, '>'); html.push("", name, ""); }, makeDOMNodeLine: function (html, setting, node) { html.push(""); }, makeDOMNodeMainAfter: function (html, setting, node) { html.push("
    • "); }, makeDOMNodeMainBefore: function (html, setting, node) { html.push("
    • "); }, makeDOMNodeNameAfter: function (html, setting, node) { html.push(""); }, makeDOMNodeNameBefore: function (html, setting, node) { var title = data.nodeTitle(setting, node), url = view.makeNodeUrl(setting, node), fontcss = view.makeNodeFontCss(setting, node), nodeClasses = view.makeNodeClasses(setting, node), fontStyle = []; for (var f in fontcss) { fontStyle.push(f, ":", fontcss[f], ";"); } html.push(" 0) ? " href='" + url + "'" : ""), " target='", view.makeNodeTarget(node), "' style='", fontStyle.join(''), "'"); if (tools.apply(setting.view.showTitle, [setting.treeId, node], setting.view.showTitle) && title) { html.push("title='", title.replace(/'/g, "'").replace(//g, '>'), "'"); } html.push(">"); }, makeNodeFontCss: function (setting, node) { var fontCss = tools.apply(setting.view.fontCss, [setting.treeId, node], setting.view.fontCss); return (fontCss && ((typeof fontCss) != "function")) ? fontCss : {}; }, makeNodeClasses: function (setting, node) { var classes = tools.apply(setting.view.nodeClasses, [setting.treeId, node], setting.view.nodeClasses); return (classes && (typeof classes !== "function")) ? classes : {add:[], remove:[]}; }, makeNodeIcoClass: function (setting, node) { var icoCss = ["ico"]; if (!node.isAjaxing) { var isParent = data.nodeIsParent(setting, node); icoCss[0] = (node.iconSkin ? node.iconSkin + "_" : "") + icoCss[0]; if (isParent) { icoCss.push(node.open ? consts.folder.OPEN : consts.folder.CLOSE); } else { icoCss.push(consts.folder.DOCU); } } return consts.className.BUTTON + " " + icoCss.join('_'); }, makeNodeIcoStyle: function (setting, node) { var icoStyle = []; if (!node.isAjaxing) { var isParent = data.nodeIsParent(setting, node); var icon = (isParent && node.iconOpen && node.iconClose) ? (node.open ? node.iconOpen : node.iconClose) : node[setting.data.key.icon]; if (icon) icoStyle.push("background:url(", icon, ") 0 0 no-repeat;"); if (setting.view.showIcon == false || !tools.apply(setting.view.showIcon, [setting.treeId, node], true)) { icoStyle.push("display:none;"); } } return icoStyle.join(''); }, makeNodeLineClass: function (setting, node) { var lineClass = []; if (setting.view.showLine) { if (node.level == 0 && node.isFirstNode && node.isLastNode) { lineClass.push(consts.line.ROOT); } else if (node.level == 0 && node.isFirstNode) { lineClass.push(consts.line.ROOTS); } else if (node.isLastNode) { lineClass.push(consts.line.BOTTOM); } else { lineClass.push(consts.line.CENTER); } } else { lineClass.push(consts.line.NOLINE); } if (data.nodeIsParent(setting, node)) { lineClass.push(node.open ? consts.folder.OPEN : consts.folder.CLOSE); } else { lineClass.push(consts.folder.DOCU); } return view.makeNodeLineClassEx(node) + lineClass.join('_'); }, makeNodeLineClassEx: function (node) { return consts.className.BUTTON + " " + consts.className.LEVEL + node.level + " " + consts.className.SWITCH + " "; }, makeNodeTarget: function (node) { return (node.target || "_blank"); }, makeNodeUrl: function (setting, node) { var urlKey = setting.data.key.url; return node[urlKey] ? node[urlKey] : null; }, makeUlHtml: function (setting, node, html, content) { html.push("
        "); html.push(content); html.push("
      "); }, makeUlLineClass: function (setting, node) { return ((setting.view.showLine && !node.isLastNode) ? consts.line.LINE : ""); }, removeChildNodes: function (setting, node) { if (!node) return; var nodes = data.nodeChildren(setting, node); if (!nodes) return; for (var i = 0, l = nodes.length; i < l; i++) { data.removeNodeCache(setting, nodes[i]); } data.removeSelectedNode(setting); delete node[setting.data.key.children]; if (!setting.data.keep.parent) { data.nodeIsParent(setting, node, false); node.open = false; var tmp_switchObj = $$(node, consts.id.SWITCH, setting), tmp_icoObj = $$(node, consts.id.ICON, setting); view.replaceSwitchClass(node, tmp_switchObj, consts.folder.DOCU); view.replaceIcoClass(node, tmp_icoObj, consts.folder.DOCU); $$(node, consts.id.UL, setting).remove(); } else { $$(node, consts.id.UL, setting).empty(); } }, scrollIntoView: function (setting, dom) { if (!dom) { return; } // support IE 7 / 8 if (typeof Element === 'undefined' || typeof HTMLElement === 'undefined') { var contRect = setting.treeObj.get(0).getBoundingClientRect(), findMeRect = dom.getBoundingClientRect(); if (findMeRect.top < contRect.top || findMeRect.bottom > contRect.bottom || findMeRect.right > contRect.right || findMeRect.left < contRect.left) { dom.scrollIntoView(); } return; } // CC-BY jocki84@googlemail.com, https://gist.github.com/jocki84/6ffafd003387179a988e if (!Element.prototype.scrollIntoViewIfNeeded) { Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { "use strict"; function makeRange(start, length) { return {"start": start, "length": length, "end": start + length}; } function coverRange(inner, outer) { if ( false === centerIfNeeded || (outer.start < inner.end && inner.start < outer.end) ) { return Math.max( inner.end - outer.length, Math.min(outer.start, inner.start) ); } return (inner.start + inner.end - outer.length) / 2; } function makePoint(x, y) { return { "x": x, "y": y, "translate": function translate(dX, dY) { return makePoint(x + dX, y + dY); } }; } function absolute(elem, pt) { while (elem) { pt = pt.translate(elem.offsetLeft, elem.offsetTop); elem = elem.offsetParent; } return pt; } var target = absolute(this, makePoint(0, 0)), extent = makePoint(this.offsetWidth, this.offsetHeight), elem = this.parentNode, origin; while (elem instanceof HTMLElement) { // Apply desired scroll amount. origin = absolute(elem, makePoint(elem.clientLeft, elem.clientTop)); elem.scrollLeft = coverRange( makeRange(target.x - origin.x, extent.x), makeRange(elem.scrollLeft, elem.clientWidth) ); elem.scrollTop = coverRange( makeRange(target.y - origin.y, extent.y), makeRange(elem.scrollTop, elem.clientHeight) ); // Determine actual scroll amount by reading back scroll properties. target = target.translate(-elem.scrollLeft, -elem.scrollTop); elem = elem.parentNode; } }; } dom.scrollIntoViewIfNeeded(); }, setFirstNode: function (setting, parentNode) { var children = data.nodeChildren(setting, parentNode); if (children.length > 0) { children[0].isFirstNode = true; } }, setLastNode: function (setting, parentNode) { var children = data.nodeChildren(setting, parentNode); if (children.length > 0) { children[children.length - 1].isLastNode = true; } }, removeNode: function (setting, node) { var root = data.getRoot(setting), parentNode = (node.parentTId) ? node.getParentNode() : root; node.isFirstNode = false; node.isLastNode = false; node.getPreNode = function () { return null; }; node.getNextNode = function () { return null; }; if (!data.getNodeCache(setting, node.tId)) { return; } $$(node, setting).remove(); data.removeNodeCache(setting, node); data.removeSelectedNode(setting, node); var children = data.nodeChildren(setting, parentNode); for (var i = 0, l = children.length; i < l; i++) { if (children[i].tId == node.tId) { children.splice(i, 1); break; } } view.setFirstNode(setting, parentNode); view.setLastNode(setting, parentNode); var tmp_ulObj, tmp_switchObj, tmp_icoObj, childLength = children.length; //repair nodes old parent if (!setting.data.keep.parent && childLength == 0) { //old parentNode has no child nodes data.nodeIsParent(setting, parentNode, false); parentNode.open = false; delete parentNode[setting.data.key.children]; tmp_ulObj = $$(parentNode, consts.id.UL, setting); tmp_switchObj = $$(parentNode, consts.id.SWITCH, setting); tmp_icoObj = $$(parentNode, consts.id.ICON, setting); view.replaceSwitchClass(parentNode, tmp_switchObj, consts.folder.DOCU); view.replaceIcoClass(parentNode, tmp_icoObj, consts.folder.DOCU); tmp_ulObj.css("display", "none"); } else if (setting.view.showLine && childLength > 0) { //old parentNode has child nodes var newLast = children[childLength - 1]; tmp_ulObj = $$(newLast, consts.id.UL, setting); tmp_switchObj = $$(newLast, consts.id.SWITCH, setting); tmp_icoObj = $$(newLast, consts.id.ICON, setting); if (parentNode == root) { if (children.length == 1) { //node was root, and ztree has only one root after move node view.replaceSwitchClass(newLast, tmp_switchObj, consts.line.ROOT); } else { var tmp_first_switchObj = $$(children[0], consts.id.SWITCH, setting); view.replaceSwitchClass(children[0], tmp_first_switchObj, consts.line.ROOTS); view.replaceSwitchClass(newLast, tmp_switchObj, consts.line.BOTTOM); } } else { view.replaceSwitchClass(newLast, tmp_switchObj, consts.line.BOTTOM); } tmp_ulObj.removeClass(consts.line.LINE); } }, replaceIcoClass: function (node, obj, newName) { if (!obj || node.isAjaxing) return; var tmpName = obj.attr("class"); if (tmpName == undefined) return; var tmpList = tmpName.split("_"); switch (newName) { case consts.folder.OPEN: case consts.folder.CLOSE: case consts.folder.DOCU: tmpList[tmpList.length - 1] = newName; break; } obj.attr("class", tmpList.join("_")); }, replaceSwitchClass: function (node, obj, newName) { if (!obj) return; var tmpName = obj.attr("class"); if (tmpName == undefined) return; var tmpList = tmpName.split("_"); switch (newName) { case consts.line.ROOT: case consts.line.ROOTS: case consts.line.CENTER: case consts.line.BOTTOM: case consts.line.NOLINE: tmpList[0] = view.makeNodeLineClassEx(node) + newName; break; case consts.folder.OPEN: case consts.folder.CLOSE: case consts.folder.DOCU: tmpList[1] = newName; break; } obj.attr("class", tmpList.join("_")); if (newName !== consts.folder.DOCU) { obj.removeAttr("disabled"); } else { obj.attr("disabled", "disabled"); } }, selectNode: function (setting, node, addFlag) { if (!addFlag) { view.cancelPreSelectedNode(setting, null, node); } $$(node, consts.id.A, setting).addClass(consts.node.CURSELECTED); data.addSelectedNode(setting, node); setting.treeObj.trigger(consts.event.SELECTED, [setting.treeId, node]); }, setNodeFontCss: function (setting, treeNode) { var aObj = $$(treeNode, consts.id.A, setting), fontCss = view.makeNodeFontCss(setting, treeNode); if (fontCss) { aObj.css(fontCss); } }, setNodeClasses: function (setting, treeNode) { var aObj = $$(treeNode, consts.id.A, setting), classes = view.makeNodeClasses(setting, treeNode); if ('add' in classes && classes.add.length) { aObj.addClass(classes.add.join(' ')); } if ('remove' in classes && classes.remove.length) { aObj.removeClass(classes.remove.join(' ')); } }, setNodeLineIcos: function (setting, node) { if (!node) return; var switchObj = $$(node, consts.id.SWITCH, setting), ulObj = $$(node, consts.id.UL, setting), icoObj = $$(node, consts.id.ICON, setting), ulLine = view.makeUlLineClass(setting, node); if (ulLine.length == 0) { ulObj.removeClass(consts.line.LINE); } else { ulObj.addClass(ulLine); } switchObj.attr("class", view.makeNodeLineClass(setting, node)); if (data.nodeIsParent(setting, node)) { switchObj.removeAttr("disabled"); } else { switchObj.attr("disabled", "disabled"); } icoObj.removeAttr("style"); icoObj.attr("style", view.makeNodeIcoStyle(setting, node)); icoObj.attr("class", view.makeNodeIcoClass(setting, node)); }, setNodeName: function (setting, node) { var title = data.nodeTitle(setting, node), nObj = $$(node, consts.id.SPAN, setting); nObj.empty(); if (setting.view.nameIsHTML) { nObj.html(data.nodeName(setting, node)); } else { nObj.text(data.nodeName(setting, node)); } if (tools.apply(setting.view.showTitle, [setting.treeId, node], setting.view.showTitle)) { var aObj = $$(node, consts.id.A, setting); aObj.attr("title", !title ? "" : title); } }, setNodeTarget: function (setting, node) { var aObj = $$(node, consts.id.A, setting); aObj.attr("target", view.makeNodeTarget(node)); }, setNodeUrl: function (setting, node) { var aObj = $$(node, consts.id.A, setting), url = view.makeNodeUrl(setting, node); if (url == null || url.length == 0) { aObj.removeAttr("href"); } else { aObj.attr("href", url); } }, switchNode: function (setting, node) { if (node.open || !tools.canAsync(setting, node)) { view.expandCollapseNode(setting, node, !node.open); } else if (setting.async.enable) { if (!view.asyncNode(setting, node)) { view.expandCollapseNode(setting, node, !node.open); return; } } else if (node) { view.expandCollapseNode(setting, node, !node.open); } } }; // zTree defind $.fn.zTree = { consts: _consts, _z: { tools: tools, view: view, event: event, data: data }, getZTreeObj: function (treeId) { var o = data.getZTreeTools(treeId); return o ? o : null; }, destroy: function (treeId) { if (!!treeId && treeId.length > 0) { view.destroy(data.getSetting(treeId)); } else { for (var s in settings) { view.destroy(settings[s]); } } }, init: function (obj, zSetting, zNodes) { var setting = tools.clone(_setting); $.extend(true, setting, zSetting); setting.treeId = obj.attr("id"); setting.treeObj = obj; setting.treeObj.empty(); settings[setting.treeId] = setting; //For some older browser,(e.g., ie6) if (typeof document.body.style.maxHeight === "undefined") { setting.view.expandSpeed = ""; } data.initRoot(setting); var root = data.getRoot(setting); zNodes = zNodes ? tools.clone(tools.isArray(zNodes) ? zNodes : [zNodes]) : []; if (setting.data.simpleData.enable) { data.nodeChildren(setting, root, data.transformTozTreeFormat(setting, zNodes)); } else { data.nodeChildren(setting, root, zNodes); } data.initCache(setting); event.unbindTree(setting); event.bindTree(setting); event.unbindEvent(setting); event.bindEvent(setting); var zTreeTools = { setting: setting, addNodes: function (parentNode, index, newNodes, isSilent) { if (!parentNode) parentNode = null; var isParent = data.nodeIsParent(setting, parentNode); if (parentNode && !isParent && setting.data.keep.leaf) return null; var i = parseInt(index, 10); if (isNaN(i)) { isSilent = !!newNodes; newNodes = index; index = -1; } else { index = i; } if (!newNodes) return null; var xNewNodes = tools.clone(tools.isArray(newNodes) ? newNodes : [newNodes]); function addCallback() { view.addNodes(setting, parentNode, index, xNewNodes, (isSilent == true)); } if (tools.canAsync(setting, parentNode)) { view.asyncNode(setting, parentNode, isSilent, addCallback); } else { addCallback(); } return xNewNodes; }, cancelSelectedNode: function (node) { view.cancelPreSelectedNode(setting, node); }, destroy: function () { view.destroy(setting); }, expandAll: function (expandFlag) { expandFlag = !!expandFlag; view.expandCollapseSonNode(setting, null, expandFlag, true); return expandFlag; }, expandNode: function (node, expandFlag, sonSign, focus, callbackFlag) { if (!node || !data.nodeIsParent(setting, node)) return null; if (expandFlag !== true && expandFlag !== false) { expandFlag = !node.open; } callbackFlag = !!callbackFlag; if (callbackFlag && expandFlag && (tools.apply(setting.callback.beforeExpand, [setting.treeId, node], true) == false)) { return null; } else if (callbackFlag && !expandFlag && (tools.apply(setting.callback.beforeCollapse, [setting.treeId, node], true) == false)) { return null; } if (expandFlag && node.parentTId) { view.expandCollapseParentNode(setting, node.getParentNode(), expandFlag, false); } if (expandFlag === node.open && !sonSign) { return null; } data.getRoot(setting).expandTriggerFlag = callbackFlag; if (!tools.canAsync(setting, node) && sonSign) { view.expandCollapseSonNode(setting, node, expandFlag, true, showNodeFocus); } else { node.open = !expandFlag; view.switchNode(this.setting, node); showNodeFocus(); } return expandFlag; function showNodeFocus() { var a = $$(node, consts.id.A, setting).get(0); if (a && focus !== false) { view.scrollIntoView(setting, a); } } }, getNodes: function () { return data.getNodes(setting); }, getNodeByParam: function (key, value, parentNode) { if (!key) return null; return data.getNodeByParam(setting, parentNode ? data.nodeChildren(setting, parentNode) : data.getNodes(setting), key, value); }, getNodeByTId: function (tId) { return data.getNodeCache(setting, tId); }, getNodesByParam: function (key, value, parentNode) { if (!key) return null; return data.getNodesByParam(setting, parentNode ? data.nodeChildren(setting, parentNode) : data.getNodes(setting), key, value); }, getNodesByParamFuzzy: function (key, value, parentNode) { if (!key) return null; return data.getNodesByParamFuzzy(setting, parentNode ? data.nodeChildren(setting, parentNode) : data.getNodes(setting), key, value); }, getNodesByFilter: function (filter, isSingle, parentNode, invokeParam) { isSingle = !!isSingle; if (!filter || (typeof filter != "function")) return (isSingle ? null : []); return data.getNodesByFilter(setting, parentNode ? data.nodeChildren(setting, parentNode) : data.getNodes(setting), filter, isSingle, invokeParam); }, getNodeIndex: function (node) { if (!node) return null; var parentNode = (node.parentTId) ? node.getParentNode() : data.getRoot(setting); var children = data.nodeChildren(setting, parentNode); for (var i = 0, l = children.length; i < l; i++) { if (children[i] == node) return i; } return -1; }, getSelectedNodes: function () { var r = [], list = data.getRoot(setting).curSelectedList; for (var i = 0, l = list.length; i < l; i++) { r.push(list[i]); } return r; }, isSelectedNode: function (node) { return data.isSelectedNode(setting, node); }, reAsyncChildNodesPromise: function (parentNode, reloadType, isSilent) { var promise = new Promise(function (resolve, reject) { try { zTreeTools.reAsyncChildNodes(parentNode, reloadType, isSilent, function () { resolve(parentNode); }); } catch (e) { reject(e); } }); return promise; }, reAsyncChildNodes: function (parentNode, reloadType, isSilent, callback) { if (!this.setting.async.enable) return; var isRoot = !parentNode; if (isRoot) { parentNode = data.getRoot(setting); } if (reloadType == "refresh") { var children = data.nodeChildren(setting, parentNode); for (var i = 0, l = children ? children.length : 0; i < l; i++) { data.removeNodeCache(setting, children[i]); } data.removeSelectedNode(setting); data.nodeChildren(setting, parentNode, []); if (isRoot) { this.setting.treeObj.empty(); } else { var ulObj = $$(parentNode, consts.id.UL, setting); ulObj.empty(); } } view.asyncNode(this.setting, isRoot ? null : parentNode, !!isSilent, callback); }, refresh: function () { this.setting.treeObj.empty(); var root = data.getRoot(setting), nodes = data.nodeChildren(setting, root); data.initRoot(setting); data.nodeChildren(setting, root, nodes); data.initCache(setting); view.createNodes(setting, 0, data.nodeChildren(setting, root), null, -1); }, removeChildNodes: function (node) { if (!node) return null; var nodes = data.nodeChildren(setting, node); view.removeChildNodes(setting, node); return nodes ? nodes : null; }, removeNode: function (node, callbackFlag) { if (!node) return; callbackFlag = !!callbackFlag; if (callbackFlag && tools.apply(setting.callback.beforeRemove, [setting.treeId, node], true) == false) return; view.removeNode(setting, node); if (callbackFlag) { this.setting.treeObj.trigger(consts.event.REMOVE, [setting.treeId, node]); } }, selectNode: function (node, addFlag, isSilent) { if (!node) return; if (tools.uCanDo(setting)) { addFlag = setting.view.selectedMulti && addFlag; if (node.parentTId) { view.expandCollapseParentNode(setting, node.getParentNode(), true, false, showNodeFocus); } else if (!isSilent) { try { $$(node, setting).focus().blur(); } catch (e) { } } view.selectNode(setting, node, addFlag); } function showNodeFocus() { if (isSilent) { return; } var a = $$(node, setting).get(0); view.scrollIntoView(setting, a); } }, transformTozTreeNodes: function (simpleNodes) { return data.transformTozTreeFormat(setting, simpleNodes); }, transformToArray: function (nodes) { return data.transformToArrayFormat(setting, nodes); }, updateNode: function (node, checkTypeFlag) { if (!node) return; var nObj = $$(node, setting); if (nObj.get(0) && tools.uCanDo(setting)) { view.setNodeName(setting, node); view.setNodeTarget(setting, node); view.setNodeUrl(setting, node); view.setNodeLineIcos(setting, node); view.setNodeFontCss(setting, node); view.setNodeClasses(setting, node); } } }; root.treeTools = zTreeTools; data.setZTreeTools(setting, zTreeTools); var children = data.nodeChildren(setting, root); if (children && children.length > 0) { view.createNodes(setting, 0, children, null, -1); } else if (setting.async.enable && setting.async.url && setting.async.url !== '') { view.asyncNode(setting); } return zTreeTools; } }; var zt = $.fn.zTree, $$ = tools.$, consts = zt.consts; })(jQuery); ================================================ FILE: xxl-job-admin/src/main/resources/static/plugins/zTree/js/jquery.ztree.excheck.js ================================================ /* * JQuery zTree excheck * v3.5.42 * http://treejs.cn/ * * Copyright (c) 2010 Hunter.z * * Licensed same as jquery - MIT License * http://www.opensource.org/licenses/mit-license.php * * Date: 2020-01-19 */ (function ($) { //default consts of excheck var _consts = { event: { CHECK: "ztree_check" }, id: { CHECK: "_check" }, checkbox: { STYLE: "checkbox", DEFAULT: "chk", DISABLED: "disable", FALSE: "false", TRUE: "true", FULL: "full", PART: "part", FOCUS: "focus" }, radio: { STYLE: "radio", TYPE_ALL: "all", TYPE_LEVEL: "level" } }, //default setting of excheck _setting = { check: { enable: false, autoCheckTrigger: false, chkStyle: _consts.checkbox.STYLE, nocheckInherit: false, chkDisabledInherit: false, radioType: _consts.radio.TYPE_LEVEL, chkboxType: { "Y": "ps", "N": "ps" } }, data: { key: { checked: "checked" } }, callback: { beforeCheck: null, onCheck: null } }, //default root of excheck _initRoot = function (setting) { var r = data.getRoot(setting); r.radioCheckedList = []; }, //default cache of excheck _initCache = function (treeId) { }, //default bind event of excheck _bindEvent = function (setting) { var o = setting.treeObj, c = consts.event; o.bind(c.CHECK, function (event, srcEvent, treeId, node) { event.srcEvent = srcEvent; tools.apply(setting.callback.onCheck, [event, treeId, node]); }); }, _unbindEvent = function (setting) { var o = setting.treeObj, c = consts.event; o.unbind(c.CHECK); }, //default event proxy of excheck _eventProxy = function (e) { var target = e.target, setting = data.getSetting(e.data.treeId), tId = "", node = null, nodeEventType = "", treeEventType = "", nodeEventCallback = null, treeEventCallback = null; if (tools.eqs(e.type, "mouseover")) { if (setting.check.enable && tools.eqs(target.tagName, "span") && target.getAttribute("treeNode" + consts.id.CHECK) !== null) { tId = tools.getNodeMainDom(target).id; nodeEventType = "mouseoverCheck"; } } else if (tools.eqs(e.type, "mouseout")) { if (setting.check.enable && tools.eqs(target.tagName, "span") && target.getAttribute("treeNode" + consts.id.CHECK) !== null) { tId = tools.getNodeMainDom(target).id; nodeEventType = "mouseoutCheck"; } } else if (tools.eqs(e.type, "click")) { if (setting.check.enable && tools.eqs(target.tagName, "span") && target.getAttribute("treeNode" + consts.id.CHECK) !== null) { tId = tools.getNodeMainDom(target).id; nodeEventType = "checkNode"; } } if (tId.length > 0) { node = data.getNodeCache(setting, tId); switch (nodeEventType) { case "checkNode" : nodeEventCallback = _handler.onCheckNode; break; case "mouseoverCheck" : nodeEventCallback = _handler.onMouseoverCheck; break; case "mouseoutCheck" : nodeEventCallback = _handler.onMouseoutCheck; break; } } var proxyResult = { stop: nodeEventType === "checkNode", node: node, nodeEventType: nodeEventType, nodeEventCallback: nodeEventCallback, treeEventType: treeEventType, treeEventCallback: treeEventCallback }; return proxyResult }, //default init node of excheck _initNode = function (setting, level, n, parentNode, isFirstNode, isLastNode, openFlag) { if (!n) return; var checked = data.nodeChecked(setting, n); n.checkedOld = checked; if (typeof n.nocheck == "string") n.nocheck = tools.eqs(n.nocheck, "true"); n.nocheck = !!n.nocheck || (setting.check.nocheckInherit && parentNode && !!parentNode.nocheck); if (typeof n.chkDisabled == "string") n.chkDisabled = tools.eqs(n.chkDisabled, "true"); n.chkDisabled = !!n.chkDisabled || (setting.check.chkDisabledInherit && parentNode && !!parentNode.chkDisabled); if (typeof n.halfCheck == "string") n.halfCheck = tools.eqs(n.halfCheck, "true"); n.halfCheck = !!n.halfCheck; n.check_Child_State = -1; n.check_Focus = false; n.getCheckStatus = function () { return data.getCheckStatus(setting, n); }; if (setting.check.chkStyle == consts.radio.STYLE && setting.check.radioType == consts.radio.TYPE_ALL && checked) { var r = data.getRoot(setting); r.radioCheckedList.push(n); } }, //add dom for check _beforeA = function (setting, node, html) { if (setting.check.enable) { data.makeChkFlag(setting, node); html.push(""); } }, //update zTreeObj, add method of check _zTreeTools = function (setting, zTreeTools) { zTreeTools.checkNode = function (node, checked, checkTypeFlag, callbackFlag) { var nodeChecked = data.nodeChecked(setting, node); if (node.chkDisabled === true) return; if (checked !== true && checked !== false) { checked = !nodeChecked; } callbackFlag = !!callbackFlag; if (nodeChecked === checked && !checkTypeFlag) { return; } else if (callbackFlag && tools.apply(this.setting.callback.beforeCheck, [this.setting.treeId, node], true) == false) { return; } if (tools.uCanDo(this.setting) && this.setting.check.enable && node.nocheck !== true) { data.nodeChecked(setting, node, checked); var checkObj = $$(node, consts.id.CHECK, this.setting); if (checkTypeFlag || this.setting.check.chkStyle === consts.radio.STYLE) view.checkNodeRelation(this.setting, node); view.setChkClass(this.setting, checkObj, node); view.repairParentChkClassWithSelf(this.setting, node); if (callbackFlag) { this.setting.treeObj.trigger(consts.event.CHECK, [null, this.setting.treeId, node]); } } } zTreeTools.checkAllNodes = function (checked) { view.repairAllChk(this.setting, !!checked); } zTreeTools.getCheckedNodes = function (checked) { checked = (checked !== false); var children = data.nodeChildren(setting, data.getRoot(this.setting)); return data.getTreeCheckedNodes(this.setting, children, checked); } zTreeTools.getChangeCheckedNodes = function () { var children = data.nodeChildren(setting, data.getRoot(this.setting)); return data.getTreeChangeCheckedNodes(this.setting, children); } zTreeTools.setChkDisabled = function (node, disabled, inheritParent, inheritChildren) { disabled = !!disabled; inheritParent = !!inheritParent; inheritChildren = !!inheritChildren; view.repairSonChkDisabled(this.setting, node, disabled, inheritChildren); view.repairParentChkDisabled(this.setting, node.getParentNode(), disabled, inheritParent); } var _updateNode = zTreeTools.updateNode; zTreeTools.updateNode = function (node, checkTypeFlag) { if (_updateNode) _updateNode.apply(zTreeTools, arguments); if (!node || !this.setting.check.enable) return; var nObj = $$(node, this.setting); if (nObj.get(0) && tools.uCanDo(this.setting)) { var checkObj = $$(node, consts.id.CHECK, this.setting); if (checkTypeFlag == true || this.setting.check.chkStyle === consts.radio.STYLE) view.checkNodeRelation(this.setting, node); view.setChkClass(this.setting, checkObj, node); view.repairParentChkClassWithSelf(this.setting, node); } } }, //method of operate data _data = { getRadioCheckedList: function (setting) { var checkedList = data.getRoot(setting).radioCheckedList; for (var i = 0, j = checkedList.length; i < j; i++) { if (!data.getNodeCache(setting, checkedList[i].tId)) { checkedList.splice(i, 1); i--; j--; } } return checkedList; }, getCheckStatus: function (setting, node) { if (!setting.check.enable || node.nocheck || node.chkDisabled) return null; var checked = data.nodeChecked(setting, node), r = { checked: checked, half: node.halfCheck ? node.halfCheck : (setting.check.chkStyle == consts.radio.STYLE ? (node.check_Child_State === 2) : (checked ? (node.check_Child_State > -1 && node.check_Child_State < 2) : (node.check_Child_State > 0))) }; return r; }, getTreeCheckedNodes: function (setting, nodes, checked, results) { if (!nodes) return []; var onlyOne = (checked && setting.check.chkStyle == consts.radio.STYLE && setting.check.radioType == consts.radio.TYPE_ALL); results = !results ? [] : results; for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; var children = data.nodeChildren(setting, node); var nodeChecked = data.nodeChecked(setting, node); if (node.nocheck !== true && node.chkDisabled !== true && nodeChecked == checked) { results.push(node); if (onlyOne) { break; } } data.getTreeCheckedNodes(setting, children, checked, results); if (onlyOne && results.length > 0) { break; } } return results; }, getTreeChangeCheckedNodes: function (setting, nodes, results) { if (!nodes) return []; results = !results ? [] : results; for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; var children = data.nodeChildren(setting, node); var nodeChecked = data.nodeChecked(setting, node); if (node.nocheck !== true && node.chkDisabled !== true && nodeChecked != node.checkedOld) { results.push(node); } data.getTreeChangeCheckedNodes(setting, children, results); } return results; }, makeChkFlag: function (setting, node) { if (!node) return; var chkFlag = -1; var children = data.nodeChildren(setting, node); if (children) { for (var i = 0, l = children.length; i < l; i++) { var cNode = children[i]; var nodeChecked = data.nodeChecked(setting, cNode); var tmp = -1; if (setting.check.chkStyle == consts.radio.STYLE) { if (cNode.nocheck === true || cNode.chkDisabled === true) { tmp = cNode.check_Child_State; } else if (cNode.halfCheck === true) { tmp = 2; } else if (nodeChecked) { tmp = 2; } else { tmp = cNode.check_Child_State > 0 ? 2 : 0; } if (tmp == 2) { chkFlag = 2; break; } else if (tmp == 0) { chkFlag = 0; } } else if (setting.check.chkStyle == consts.checkbox.STYLE) { if (cNode.nocheck === true || cNode.chkDisabled === true) { tmp = cNode.check_Child_State; } else if (cNode.halfCheck === true) { tmp = 1; } else if (nodeChecked) { tmp = (cNode.check_Child_State === -1 || cNode.check_Child_State === 2) ? 2 : 1; } else { tmp = (cNode.check_Child_State > 0) ? 1 : 0; } if (tmp === 1) { chkFlag = 1; break; } else if (tmp === 2 && chkFlag > -1 && i > 0 && tmp !== chkFlag) { chkFlag = 1; break; } else if (chkFlag === 2 && tmp > -1 && tmp < 2) { chkFlag = 1; break; } else if (tmp > -1) { chkFlag = tmp; } } } } node.check_Child_State = chkFlag; } }, //method of event proxy _event = {}, //method of event handler _handler = { onCheckNode: function (event, node) { if (node.chkDisabled === true) return false; var setting = data.getSetting(event.data.treeId); if (tools.apply(setting.callback.beforeCheck, [setting.treeId, node], true) == false) return true; var nodeChecked = data.nodeChecked(setting, node); data.nodeChecked(setting, node, !nodeChecked); view.checkNodeRelation(setting, node); var checkObj = $$(node, consts.id.CHECK, setting); view.setChkClass(setting, checkObj, node); view.repairParentChkClassWithSelf(setting, node); setting.treeObj.trigger(consts.event.CHECK, [event, setting.treeId, node]); return true; }, onMouseoverCheck: function (event, node) { if (node.chkDisabled === true) return false; var setting = data.getSetting(event.data.treeId), checkObj = $$(node, consts.id.CHECK, setting); node.check_Focus = true; view.setChkClass(setting, checkObj, node); return true; }, onMouseoutCheck: function (event, node) { if (node.chkDisabled === true) return false; var setting = data.getSetting(event.data.treeId), checkObj = $$(node, consts.id.CHECK, setting); node.check_Focus = false; view.setChkClass(setting, checkObj, node); return true; } }, //method of tools for zTree _tools = {}, //method of operate ztree dom _view = { checkNodeRelation: function (setting, node) { var pNode, i, l, r = consts.radio; var nodeChecked = data.nodeChecked(setting, node); if (setting.check.chkStyle == r.STYLE) { var checkedList = data.getRadioCheckedList(setting); if (nodeChecked) { if (setting.check.radioType == r.TYPE_ALL) { for (i = checkedList.length - 1; i >= 0; i--) { pNode = checkedList[i]; var pNodeChecked = data.nodeChecked(setting, pNode); if (pNodeChecked && pNode != node) { data.nodeChecked(setting, pNode, false); checkedList.splice(i, 1); view.setChkClass(setting, $$(pNode, consts.id.CHECK, setting), pNode); if (pNode.parentTId != node.parentTId) { view.repairParentChkClassWithSelf(setting, pNode); } } } checkedList.push(node); } else { var parentNode = (node.parentTId) ? node.getParentNode() : data.getRoot(setting); var children = data.nodeChildren(setting, parentNode); for (i = 0, l = children.length; i < l; i++) { pNode = children[i]; var pNodeChecked = data.nodeChecked(setting, pNode); if (pNodeChecked && pNode != node) { data.nodeChecked(setting, pNode, false); view.setChkClass(setting, $$(pNode, consts.id.CHECK, setting), pNode); } } } } else if (setting.check.radioType == r.TYPE_ALL) { for (i = 0, l = checkedList.length; i < l; i++) { if (node == checkedList[i]) { checkedList.splice(i, 1); break; } } } } else { var children = data.nodeChildren(setting, node); if (nodeChecked && (!children || children.length == 0 || setting.check.chkboxType.Y.indexOf("s") > -1)) { view.setSonNodeCheckBox(setting, node, true); } if (!nodeChecked && (!children || children.length == 0 || setting.check.chkboxType.N.indexOf("s") > -1)) { view.setSonNodeCheckBox(setting, node, false); } if (nodeChecked && setting.check.chkboxType.Y.indexOf("p") > -1) { view.setParentNodeCheckBox(setting, node, true); } if (!nodeChecked && setting.check.chkboxType.N.indexOf("p") > -1) { view.setParentNodeCheckBox(setting, node, false); } } }, makeChkClass: function (setting, node) { var c = consts.checkbox, r = consts.radio, fullStyle = ""; var nodeChecked = data.nodeChecked(setting, node); if (node.chkDisabled === true) { fullStyle = c.DISABLED; } else if (node.halfCheck) { fullStyle = c.PART; } else if (setting.check.chkStyle == r.STYLE) { fullStyle = (node.check_Child_State < 1) ? c.FULL : c.PART; } else { fullStyle = nodeChecked ? ((node.check_Child_State === 2 || node.check_Child_State === -1) ? c.FULL : c.PART) : ((node.check_Child_State < 1) ? c.FULL : c.PART); } var chkName = setting.check.chkStyle + "_" + (nodeChecked ? c.TRUE : c.FALSE) + "_" + fullStyle; chkName = (node.check_Focus && node.chkDisabled !== true) ? chkName + "_" + c.FOCUS : chkName; return consts.className.BUTTON + " " + c.DEFAULT + " " + chkName; }, repairAllChk: function (setting, checked) { if (setting.check.enable && setting.check.chkStyle === consts.checkbox.STYLE) { var root = data.getRoot(setting); var children = data.nodeChildren(setting, root); for (var i = 0, l = children.length; i < l; i++) { var node = children[i]; if (node.nocheck !== true && node.chkDisabled !== true) { data.nodeChecked(setting, node, checked); } view.setSonNodeCheckBox(setting, node, checked); } } }, repairChkClass: function (setting, node) { if (!node) return; data.makeChkFlag(setting, node); if (node.nocheck !== true) { var checkObj = $$(node, consts.id.CHECK, setting); view.setChkClass(setting, checkObj, node); } }, repairParentChkClass: function (setting, node) { if (!node || !node.parentTId) return; var pNode = node.getParentNode(); view.repairChkClass(setting, pNode); view.repairParentChkClass(setting, pNode); }, repairParentChkClassWithSelf: function (setting, node) { if (!node) return; var children = data.nodeChildren(setting, node); if (children && children.length > 0) { view.repairParentChkClass(setting, children[0]); } else { view.repairParentChkClass(setting, node); } }, repairSonChkDisabled: function (setting, node, chkDisabled, inherit) { if (!node) return; if (node.chkDisabled != chkDisabled) { node.chkDisabled = chkDisabled; } view.repairChkClass(setting, node); var children = data.nodeChildren(setting, node); if (children && inherit) { for (var i = 0, l = children.length; i < l; i++) { var sNode = children[i]; view.repairSonChkDisabled(setting, sNode, chkDisabled, inherit); } } }, repairParentChkDisabled: function (setting, node, chkDisabled, inherit) { if (!node) return; if (node.chkDisabled != chkDisabled && inherit) { node.chkDisabled = chkDisabled; } view.repairChkClass(setting, node); view.repairParentChkDisabled(setting, node.getParentNode(), chkDisabled, inherit); }, setChkClass: function (setting, obj, node) { if (!obj) return; if (node.nocheck === true) { obj.hide(); } else { obj.show(); } obj.attr('class', view.makeChkClass(setting, node)); }, setParentNodeCheckBox: function (setting, node, value, srcNode) { var checkObj = $$(node, consts.id.CHECK, setting); if (!srcNode) srcNode = node; data.makeChkFlag(setting, node); if (node.nocheck !== true && node.chkDisabled !== true) { data.nodeChecked(setting, node, value); view.setChkClass(setting, checkObj, node); if (setting.check.autoCheckTrigger && node != srcNode) { setting.treeObj.trigger(consts.event.CHECK, [null, setting.treeId, node]); } } if (node.parentTId) { var pSign = true; if (!value) { var pNodes = data.nodeChildren(setting, node.getParentNode()); for (var i = 0, l = pNodes.length; i < l; i++) { var pNode = pNodes[i]; var nodeChecked = data.nodeChecked(setting, pNode); if ((pNode.nocheck !== true && pNode.chkDisabled !== true && nodeChecked) || ((pNode.nocheck === true || pNode.chkDisabled === true) && pNode.check_Child_State > 0)) { pSign = false; break; } } } if (pSign) { view.setParentNodeCheckBox(setting, node.getParentNode(), value, srcNode); } } }, setSonNodeCheckBox: function (setting, node, value, srcNode) { if (!node) return; var checkObj = $$(node, consts.id.CHECK, setting); if (!srcNode) srcNode = node; var hasDisable = false; var children = data.nodeChildren(setting, node); if (children) { for (var i = 0, l = children.length; i < l; i++) { var sNode = children[i]; view.setSonNodeCheckBox(setting, sNode, value, srcNode); if (sNode.chkDisabled === true) hasDisable = true; } } if (node != data.getRoot(setting) && node.chkDisabled !== true) { if (hasDisable && node.nocheck !== true) { data.makeChkFlag(setting, node); } if (node.nocheck !== true && node.chkDisabled !== true) { data.nodeChecked(setting, node, value); if (!hasDisable) node.check_Child_State = (children && children.length > 0) ? (value ? 2 : 0) : -1; } else { node.check_Child_State = -1; } view.setChkClass(setting, checkObj, node); if (setting.check.autoCheckTrigger && node != srcNode && node.nocheck !== true && node.chkDisabled !== true) { setting.treeObj.trigger(consts.event.CHECK, [null, setting.treeId, node]); } } } }, _z = { tools: _tools, view: _view, event: _event, data: _data }; $.extend(true, $.fn.zTree.consts, _consts); $.extend(true, $.fn.zTree._z, _z); var zt = $.fn.zTree, tools = zt._z.tools, consts = zt.consts, view = zt._z.view, data = zt._z.data, event = zt._z.event, $$ = tools.$; data.nodeChecked = function (setting, node, newChecked) { if (!node) { return false; } var key = setting.data.key.checked; if (typeof newChecked !== 'undefined') { if (typeof newChecked === "string") { newChecked = tools.eqs(newChecked, "true"); } newChecked = !!newChecked; node[key] = newChecked; } else if (typeof node[key] == "string"){ node[key] = tools.eqs(node[key], "true"); } else { node[key] = !!node[key]; } return node[key]; }; data.exSetting(_setting); data.addInitBind(_bindEvent); data.addInitUnBind(_unbindEvent); data.addInitCache(_initCache); data.addInitNode(_initNode); data.addInitProxy(_eventProxy, true); data.addInitRoot(_initRoot); data.addBeforeA(_beforeA); data.addZTreeTools(_zTreeTools); var _createNodes = view.createNodes; view.createNodes = function (setting, level, nodes, parentNode, index) { if (_createNodes) _createNodes.apply(view, arguments); if (!nodes) return; view.repairParentChkClassWithSelf(setting, parentNode); } var _removeNode = view.removeNode; view.removeNode = function (setting, node) { var parentNode = node.getParentNode(); if (_removeNode) _removeNode.apply(view, arguments); if (!node || !parentNode) return; view.repairChkClass(setting, parentNode); view.repairParentChkClass(setting, parentNode); } var _appendNodes = view.appendNodes; view.appendNodes = function (setting, level, nodes, parentNode, index, initFlag, openFlag) { var html = ""; if (_appendNodes) { html = _appendNodes.apply(view, arguments); } if (parentNode) { data.makeChkFlag(setting, parentNode); } return html; } })(jQuery); ================================================ FILE: xxl-job-admin/src/main/resources/templates/base/dashboard.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      <#-- 2-biz start -->
      <#-- 任务信息 -->
      ${I18n.job_dashboard_job_num} ${jobInfoCount}
      ${I18n.job_dashboard_job_num_tip}
      <#-- 调度信息 -->
      ${I18n.job_dashboard_trigger_num} ${jobLogCount}
      ${I18n.job_dashboard_trigger_num_tip} <#--<#if jobLogCount gt 0> 调度成功率:${(jobLogSuccessCount*100/jobLogCount)?string("0.00")}% -->
      <#-- 执行器 -->
      ${I18n.job_dashboard_jobgroup_num} ${executorCount}
      ${I18n.job_dashboard_jobgroup_num_tip}
      <#-- 调度报表:时间区间筛选,左侧折线图 + 右侧饼图 -->

      ${I18n.job_dashboard_report}

      <#---->
      <#---->
      <#-- 左侧折线图 -->
      <#-- 右侧饼图 -->
      <#-- 2-biz end -->
      <@netCommon.commonScript /> ================================================ FILE: xxl-job-admin/src/main/resources/templates/base/help.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      <@netCommon.commonScript /> ================================================ FILE: xxl-job-admin/src/main/resources/templates/base/index.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      Powered by XXL-JOB ${I18n.admin_version}
      <@netCommon.commonScript /> ================================================ FILE: xxl-job-admin/src/main/resources/templates/base/login.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle /> <@netCommon.commonScript /> ================================================ FILE: xxl-job-admin/src/main/resources/templates/biz/group.list.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      <#-- 查询区域 -->
      AppName
      ${I18n.jobgroup_field_title}
      <#-- 数据表格区域 -->
      <@netCommon.commonScript /> <#-- admin table --> ================================================ FILE: xxl-job-admin/src/main/resources/templates/biz/job.code.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle /> ${I18n.admin_name}
      Powered by XXL-JOB ${I18n.admin_version}
      <#-- glueModel --> <#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_PYTHON2" > <#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" /> <#-- script --> <@netCommon.commonScript /> <#-- glue ide --> <#if glueTypeModeSrc02?exists> ================================================ FILE: xxl-job-admin/src/main/resources/templates/biz/job.list.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      <#-- 查询区域 -->
      ${I18n.jobinfo_field_jobgroup}
      <#-- 数据表格区域 -->
      <#-- add --> <#-- update --> <#-- GLUE IDE:'BEAN' != row.glueType --> <#-- delete --> | <#-- 启动 --> <#-- 停止 --> | <#-- 执行一次 --> <#-- 执行日志:base_url +'/joblog?jobId='+ row.id --> <#-- 注册节点 --> <#-- 下次执行时间:row.scheduleType == 'CRON' || row.scheduleType == 'FIX_RATE' -->
      <#-- trigger -->
      <@netCommon.commonScript /> <#-- admin table --> <#-- admin util --> <#-- moment --> <#-- cronGen --> ================================================ FILE: xxl-job-admin/src/main/resources/templates/biz/log.detail.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      				
    • Powered by XXL-JOB ${I18n.admin_version}
      <@netCommon.commonScript /> ================================================ FILE: xxl-job-admin/src/main/resources/templates/biz/log.list.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      <#-- 查询区域 -->
      ${I18n.jobinfo_field_jobgroup}
      ${I18n.jobinfo_job}
      ${I18n.joblog_status}
      ${I18n.joblog_field_triggerTime}
      <#-- 数据表格区域 -->
      <@netCommon.commonScript /> <#--daterangepicker--> <#-- admin table --> ================================================ FILE: xxl-job-admin/src/main/resources/templates/biz/user.list.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> <@netCommon.commonStyle />
      <#-- 查询区域 -->
      ${I18n.user_role}
      ${I18n.user_username}
      <#-- 数据表格区域 -->
      <@netCommon.commonScript /> <#-- admin table --> ================================================ FILE: xxl-job-admin/src/main/resources/templates/common/common.errorpage.ftl ================================================ <#-- import macro --> <#import "../common/common.macro.ftl" as netCommon> Error

      System Error

      <#if exceptionMsg?exists>${exceptionMsg}<#else>Unknown Error.

      Back

      ================================================ FILE: xxl-job-admin/src/main/resources/templates/common/common.macro.ftl ================================================ <#-- import: style --> <#macro commonStyle> <#-- i18n --> <#global I18n = I18nUtil.getMultString()?eval /> <#-- title、favicon、meta --> ${I18n.admin_name_full} <#-- css --> <#-- import: script --> <#macro commonScript> ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java ================================================ 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(); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java ================================================ package com.xxl.job.admin.controller; import com.xxl.sso.core.constant.Const; import jakarta.servlet.http.Cookie; 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 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("/auth/doLogin") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("userName", "admin") .param("password", "123456") ).andReturn(); cookie = ret.getResponse().getCookie(Const.XXL_SSO_TOKEN); } @Test public void testAdd() throws Exception { MultiValueMap parameters = new LinkedMultiValueMap(); 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()); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/CronExpressionTest.java ================================================ package com.xxl.job.admin.core.util; import com.xxl.job.admin.scheduler.cron.CronExpression; import com.xxl.tool.core.DateTool; import org.junit.jupiter.api.Test; import java.text.ParseException; import java.util.Date; public class CronExpressionTest { @Test public void shouldWriteValueAsString() throws ParseException { CronExpression cronExpression = new CronExpression("0 0 0 ? * 1"); Date lastTriggerTime = new Date(); for (int i = 0; i < 5; i++) { Date nextTriggerTime = cronExpression.getNextValidTimeAfter(lastTriggerTime); System.out.println(DateTool.formatDateTime(nextTriggerTime)); lastTriggerTime = nextTriggerTime; } } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java ================================================ //package com.xxl.job.admin.core.util; // //import com.xxl.job.admin.util.JacksonUtil; //import org.junit.jupiter.api.Test; // //import java.util.HashMap; //import java.util.Map; // //import static com.xxl.job.admin.util.JacksonUtil.writeValueAsString; //import static org.junit.jupiter.api.Assertions.assertEquals; // //public class JacksonUtilTest { // // @Test // public void shouldWriteValueAsString() { // //given // Map 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"); // // } //} ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/mapper/XxlJobGroupMapperTest.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobGroup; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class XxlJobGroupMapperTest { @Resource private XxlJobGroupMapper xxlJobGroupMapper; @Test public void test(){ List list = xxlJobGroupMapper.findAll(); List list2 = xxlJobGroupMapper.findByAddressType(0); XxlJobGroup group = new XxlJobGroup(); group.setAppname("setAppName"); group.setTitle("setTitle"); group.setAddressType(0); group.setAddressList("setAddressList"); group.setUpdateTime(new Date()); int ret = xxlJobGroupMapper.save(group); XxlJobGroup group2 = xxlJobGroupMapper.load(group.getId()); group2.setAppname("setAppName2"); group2.setTitle("setTitle2"); group2.setAddressType(2); group2.setAddressList("setAddressList2"); group2.setUpdateTime(new Date()); int ret2 = xxlJobGroupMapper.update(group2); int ret3 = xxlJobGroupMapper.remove(group.getId()); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/mapper/XxlJobInfoMapperTest.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobInfo; import com.xxl.job.admin.scheduler.misfire.MisfireStrategyEnum; import com.xxl.job.admin.scheduler.type.ScheduleTypeEnum; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class XxlJobInfoMapperTest { private static Logger logger = LoggerFactory.getLogger(XxlJobInfoMapperTest.class); @Resource private XxlJobInfoMapper xxlJobInfoMapper; @Test public void pageList(){ List list = xxlJobInfoMapper.pageList(0, 20, 0, -1, null, null, null); int list_count = xxlJobInfoMapper.pageListCount(0, 20, 0, -1, null, null, null); logger.info("", list); logger.info("", list_count); List list2 = xxlJobInfoMapper.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 = xxlJobInfoMapper.save(info); XxlJobInfo info2 = xxlJobInfoMapper.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 = xxlJobInfoMapper.update(info2); xxlJobInfoMapper.delete(info2.getId()); List list2 = xxlJobInfoMapper.getJobsByGroup(1); int ret3 = xxlJobInfoMapper.findAllCount(); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/mapper/XxlJobLogGlueMapperTest.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobLogGlue; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class XxlJobLogGlueMapperTest { @Resource private XxlJobLogGlueMapper xxlJobLogGlueMapper; @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 = xxlJobLogGlueMapper.save(logGlue); List list = xxlJobLogGlueMapper.findByJobId(1); int ret2 = xxlJobLogGlueMapper.removeOld(1, 1); int ret3 = xxlJobLogGlueMapper.deleteByJobId(1); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/mapper/XxlJobLogMapperTest.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobLog; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class XxlJobLogMapperTest { @Resource private XxlJobLogMapper xxlJobLogMapper; @Test public void test(){ List list = xxlJobLogMapper.pageList(0, 10, 1, 1, null, null, 1); int list_count = xxlJobLogMapper.pageListCount(0, 10, 1, 1, null, null, 1); XxlJobLog log = new XxlJobLog(); log.setJobGroup(1); log.setJobId(1); long ret1 = xxlJobLogMapper.save(log); XxlJobLog dto = xxlJobLogMapper.load(log.getId()); log.setTriggerTime(new Date()); log.setTriggerCode(1); log.setTriggerMsg("1"); log.setExecutorAddress("1"); log.setExecutorHandler("1"); log.setExecutorParam("1"); ret1 = xxlJobLogMapper.updateTriggerInfo(log); dto = xxlJobLogMapper.load(log.getId()); log.setHandleTime(new Date()); log.setHandleCode(2); log.setHandleMsg("2"); ret1 = xxlJobLogMapper.updateHandleInfo(log); dto = xxlJobLogMapper.load(log.getId()); List ret4 = xxlJobLogMapper.findClearLogIds(1, 1, new Date(), 100, 100); int ret2 = xxlJobLogMapper.delete(log.getJobId()); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/mapper/XxlJobLogReportMapperTest.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobLogReport; import com.xxl.tool.core.DateTool; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class XxlJobLogReportMapperTest { private static final Logger logger = LoggerFactory.getLogger(XxlJobLogMapperTest.class); @Resource private XxlJobLogReportMapper xxlJobLogReportMapper; @Test public void test(){ Date date = DateTool.parseDate("2025-10-01"); XxlJobLogReport xxlJobLogReport = new XxlJobLogReport(); xxlJobLogReport.setTriggerDay(date); xxlJobLogReport.setRunningCount(444); xxlJobLogReport.setSucCount(555); xxlJobLogReport.setFailCount(666); int ret = xxlJobLogReportMapper.saveOrUpdate(xxlJobLogReport); logger.info("ret:{}", ret); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/mapper/XxlJobRegistryMapperTest.java ================================================ package com.xxl.job.admin.mapper; import com.xxl.job.admin.model.XxlJobRegistry; import com.xxl.job.core.constant.RegistType; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class XxlJobRegistryMapperTest { @Resource private XxlJobRegistryMapper xxlJobRegistryMapper; @Test public void test(){ int ret = xxlJobRegistryMapper.registrySaveOrUpdate(RegistType.EXECUTOR.name(), "xxl-job-executor-z1", "v1", new Date()); /*int ret = xxlJobRegistryDao.registryUpdate("g1", "k1", "v1", new Date()); if (ret < 1) { ret = xxlJobRegistryDao.registrySave("g1", "k1", "v1", new Date()); }*/ List list = xxlJobRegistryMapper.findAll(1, new Date()); int ret2 = xxlJobRegistryMapper.removeDead(Arrays.asList(1)); } @Test public void test2() throws InterruptedException { for (int i = 0; i < 100; i++) { new Thread(()->{ int ret = xxlJobRegistryMapper.registrySaveOrUpdate("g1", "k1", "v1", new Date()); System.out.println(ret); /*int ret = xxlJobRegistryDao.registryUpdate("g1", "k1", "v1", new Date()); if (ret < 1) { ret = xxlJobRegistryDao.registrySave("g1", "k1", "v1", new Date()); }*/ }).start(); } TimeUnit.SECONDS.sleep(10); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/schedule/JobScheduleTest.java ================================================ package com.xxl.job.admin.schedule; import com.xxl.job.admin.scheduler.config.XxlJobAdminBootstrap; import com.xxl.tool.core.DateTool; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import java.util.Date; import java.util.concurrent.TimeUnit; @SpringBootTest public class JobScheduleTest { private static Logger logger = LoggerFactory.getLogger(JobScheduleTest.class); @Test public void test() throws InterruptedException { // thread for (int i = 0; i < 10; i++) { int finalI = i; new Thread(() -> { lockTest("threadName-" + finalI); }).start(); } TimeUnit.MINUTES.sleep(10); } private void lockTest(String threadName) { TransactionStatus transactionStatus = XxlJobAdminBootstrap.getInstance().getTransactionManager().getTransaction(new DefaultTransactionDefinition()); try { String lockedRecord = XxlJobAdminBootstrap.getInstance().getXxlJobLockMapper().scheduleLock(); // for update logger.info(threadName + " : start at " + DateTool.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS") ); TimeUnit.MILLISECONDS.sleep(500); logger.info(threadName + " : end at " + DateTool.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS") ); } catch (Throwable e) { logger.error("error: ", e); } finally { logger.info(threadName + " : commit at " + DateTool.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS") ); XxlJobAdminBootstrap.getInstance().getTransactionManager().commit(transactionStatus); } } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java ================================================ package com.xxl.job.admin.util; 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()); } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/openapi/AdminBizTest.java ================================================ package com.xxl.job.openapi; import com.xxl.job.core.constant.RegistType; import com.xxl.job.core.openapi.AdminBiz; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.openapi.model.RegistryRequest; import com.xxl.job.core.context.XxlJobContext; import com.xxl.job.core.constant.Const; import com.xxl.tool.http.HttpTool; import com.xxl.tool.response.Response; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 { private static final Logger logger = LoggerFactory.getLogger(AdminBizTest.class); private static String addressUrl = "http://127.0.0.1:8080/xxl-job-admin"; private static String accessToken = "default_token"; private AdminBiz buildClient(){ String finalUrl = addressUrl + "/api"; return HttpTool.createClient() .url(finalUrl) .timeout(3 * 1000) .header(Const.XXL_JOB_ACCESS_TOKEN, accessToken) .proxy(AdminBiz.class); } @Test public void callback() throws Exception { AdminBiz adminBiz = buildClient(); CallbackRequest param = new CallbackRequest(); param.setLogId(1); param.setHandleCode(XxlJobContext.HANDLE_CODE_SUCCESS); List callbackParamList = Arrays.asList(param); Response returnT = adminBiz.callback(callbackParamList); assertTrue(returnT.isSuccess()); } /** * registry executor * * @throws Exception */ @Test public void registry() throws Exception { AdminBiz adminBiz = buildClient(); RegistryRequest registryParam = new RegistryRequest(RegistType.EXECUTOR.name(), "xxl-job-executor-example", "127.0.0.1:9999"); Response returnT = adminBiz.registry(registryParam); assertTrue(returnT.isSuccess()); } /** * registry executor remove * * @throws Exception */ @Test public void registryRemove() throws Exception { AdminBiz adminBiz = buildClient(); RegistryRequest registryParam = new RegistryRequest(RegistType.EXECUTOR.name(), "xxl-job-executor-example", "127.0.0.1:9999"); Response returnT = adminBiz.registryRemove(registryParam); assertTrue(returnT.isSuccess()); } // ---------------------- job opt ---------------------- @Test public void jobManage() throws Exception { // jobAdd、jobUpdate、jobRemove、jobStart、jobStop } } ================================================ FILE: xxl-job-admin/src/test/java/com/xxl/job/openapi/ExecutorBizTest.java ================================================ package com.xxl.job.openapi; import com.xxl.job.core.constant.Const; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.model.*; import com.xxl.job.core.constant.ExecutorBlockStrategyEnum; import com.xxl.job.core.glue.GlueTypeEnum; import com.xxl.tool.http.HttpTool; import com.xxl.tool.response.Response; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * executor api test * * Created by xuxueli on 17/5/12. */ public class ExecutorBizTest { private static final Logger logger = LoggerFactory.getLogger(ExecutorBizTest.class); private static String addressUrl = "http://127.0.0.1:9999/"; private static String accessToken = "default_token"; private ExecutorBiz buildClient(){ return HttpTool.createClient() .url(addressUrl) .timeout(3 * 1000) .header(Const.XXL_JOB_ACCESS_TOKEN, accessToken) .proxy(ExecutorBiz.class); } @Test public void beat() throws Exception { ExecutorBiz executorBiz = buildClient(); // Act final Response retval = executorBiz.beat(); // Assert result Assertions.assertNotNull(retval); Assertions.assertNull(((Response) retval).getData()); Assertions.assertEquals(200, retval.getCode()); Assertions.assertNull(retval.getMsg()); } @Test public void idleBeat(){ ExecutorBiz executorBiz = buildClient(); final int jobId = 0; // Act final Response retval = executorBiz.idleBeat(new IdleBeatRequest(jobId)); // Assert result Assertions.assertNotNull(retval); Assertions.assertNull(((Response) retval).getData()); Assertions.assertEquals(500, retval.getCode()); Assertions.assertEquals("job thread is running or has trigger queue.", retval.getMsg()); } @Test public void run(){ ExecutorBiz executorBiz = buildClient(); // trigger data final TriggerRequest triggerParam = new TriggerRequest(); 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 Response retval = executorBiz.run(triggerParam); // Assert result Assertions.assertNotNull(retval); } @Test public void kill(){ ExecutorBiz executorBiz = buildClient(); final int jobId = 0; // Act final Response retval = executorBiz.kill(new KillRequest(jobId)); // Assert result Assertions.assertNotNull(retval); Assertions.assertNull(((Response) retval).getData()); Assertions.assertEquals(200, retval.getCode()); Assertions.assertNull(retval.getMsg()); } @Test public void log(){ ExecutorBiz executorBiz = buildClient(); final long logDateTim = 0L; final long logId = 0; final int fromLineNum = 0; // Act final Response retval = executorBiz.log(new LogRequest(logDateTim, logId, fromLineNum)); // Assert result Assertions.assertNotNull(retval); } } ================================================ FILE: xxl-job-core/pom.xml ================================================ 4.0.0 com.xuxueli xxl-job 3.4.0-SNAPSHOT xxl-job-core jar ${project.artifactId} A distributed task scheduling framework. https://www.xuxueli.com/ org.slf4j slf4j-api jakarta.annotation jakarta.annotation-api provided io.netty netty-codec-http com.google.code.gson gson com.xuxueli xxl-tool org.apache.groovy groovy org.springframework spring-context provided ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/constant/Const.java ================================================ package com.xxl.job.core.constant; /** * Created by xuxueli on 17/5/10. */ public class Const { // ---------------------- for openapi ---------------------- /** * access token */ public static final String XXL_JOB_ACCESS_TOKEN = "XXL-JOB-ACCESS-TOKEN"; // ---------------------- for registry ---------------------- /** * registry beat interval, default 30s */ public static final int BEAT_TIMEOUT = 30; /** * registry dead timeout, default 90s */ public static final int DEAD_TIMEOUT = BEAT_TIMEOUT * 3; } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/constant/ExecutorBlockStrategyEnum.java ================================================ package com.xxl.job.core.constant; /** * 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; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/constant/RegistType.java ================================================ package com.xxl.job.core.constant; /** * Created by xuxueli on 17/5/9. */ public enum RegistType{ /** * executor registry */ EXECUTOR, /** * admin registry */ ADMIN; } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java ================================================ 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 ---------------------- /** * log id */ private final long logId; /** * log timestamp */ private final long logDateTime; /** * log filename */ private final String logFileName; // ---------------------- 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, long logId, long logDateTime, String logFileName, int shardIndex, int shardTotal) { this.jobId = jobId; this.jobParam = jobParam; this.logId = logId; this.logDateTime = logDateTime; this.logFileName = logFileName; this.shardIndex = shardIndex; this.shardTotal = shardTotal; this.handleCode = HANDLE_CODE_SUCCESS; // default success } public long getJobId() { return jobId; } public String getJobParam() { return jobParam; } public long getLogId() { return logId; } public long getLogDateTime() { return logDateTime; } public String getLogFileName() { return logFileName; } 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 ---------------------- /** * xxl-job context store */ private static final InheritableThreadLocal contextHolder = new InheritableThreadLocal(); // support for child thread of job handler) /** * set xxl-job context */ public static void setXxlJobContext(XxlJobContext xxlJobContext){ contextHolder.set(xxlJobContext); } /** * get xxl-job context */ public static XxlJobContext getXxlJobContext(){ return contextHolder.get(); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java ================================================ package com.xxl.job.core.context; import com.xxl.job.core.log.XxlJobFileAppender; import com.xxl.tool.core.DateTool; 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 { // ---------------------- job info ---------------------- /** * current JobId * * @return jobId */ public static long getJobId() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return -1; } return xxlJobContext.getJobId(); } /** * current JobParam * * @return jobParam */ public static String getJobParam() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return null; } return xxlJobContext.getJobParam(); } // ---------------------- log info ---------------------- /** * current job log time * * @return logDateTime */ public static long getLogId() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return -1; } return xxlJobContext.getLogId(); } /** * current job log time * * @return logDateTime */ public static long getLogDateTime() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return -1; } return xxlJobContext.getLogDateTime(); } /** * current job log filename * * @return logFileName */ public static String getLogFileName() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return null; } return xxlJobContext.getLogFileName(); } // ---------------------- shard info ---------------------- /** * current ShardIndex * * @return shardIndex */ public static int getShardIndex() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return -1; } return xxlJobContext.getShardIndex(); } /** * current ShardTotal * * @return shardTotal */ public static int getShardTotal() { XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); if (xxlJobContext == null) { return -1; } return xxlJobContext.getShardTotal(); } // ---------------------- tool for log ---------------------- private static final 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 exception to log * return true if log success */ 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 call info * @param appendLog append log */ 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];*/ String formatAppendLog = DateTool.formatDateTime(new Date()) + " " + "[" + callInfo.getClassName() + "#" + callInfo.getMethodName() + "]" + "-" + "[" + callInfo.getLineNumber() + "]" + "-" + "[" + Thread.currentThread().getName() + "]" + " " + (appendLog != null ? appendLog : ""); // appendlog String logFileName = xxlJobContext.getLogFileName(); if (logFileName!=null && !logFileName.trim().isEmpty()) { XxlJobFileAppender.appendLog(logFileName, formatAppendLog); return true; } else { logger.info(">>>>>>>>>>> {}", formatAppendLog); return false; } } // ---------------------- tool for handleResult ---------------------- /** * handle success * * @return true if handle success */ public static boolean handleSuccess(){ return handleResult(XxlJobContext.HANDLE_CODE_SUCCESS, null); } /** * handle success with log msg * * @param handleMsg log msg * @return true if handle success */ public static boolean handleSuccess(String handleMsg) { return handleResult(XxlJobContext.HANDLE_CODE_SUCCESS, handleMsg); } /** * handle fail * * @return true if handle fail */ public static boolean handleFail(){ return handleResult(XxlJobContext.HANDLE_CODE_FAIL, null); } /** * handle fail with log msg * * @param handleMsg log msg * @return true if handle fail */ public static boolean handleFail(String handleMsg) { return handleResult(XxlJobContext.HANDLE_CODE_FAIL, handleMsg); } /** * handle timeout * * @return true if handle timeout */ public static boolean handleTimeout(){ return handleResult(XxlJobContext.HANDLE_CODE_TIMEOUT, null); } /** * handle timeout with log msg * * @param handleMsg log msg * @return true if handle timeout */ public static boolean handleTimeout(String handleMsg){ return handleResult(XxlJobContext.HANDLE_CODE_TIMEOUT, handleMsg); } /** * @param handleCode * * 200 : success * 500 : fail * 502 : timeout * * @param handleMsg log msg * @return true if handle success */ 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; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java ================================================ package com.xxl.job.core.executor; import com.xxl.job.core.constant.Const; import com.xxl.job.core.openapi.AdminBiz; 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.tool.core.StringTool; import com.xxl.tool.http.HttpTool; import com.xxl.tool.http.IPTool; 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; import java.util.concurrent.TimeUnit; /** * Created by xuxueli on 2016/3/2 21:14. */ public class XxlJobExecutor { private static final Logger logger = LoggerFactory.getLogger(XxlJobExecutor.class); /* * elegant shutdown wait seconds */ private static final long ELEGANT_SHUTDOWN_WAITING_SECONDS = 5; // ---------------------- field ---------------------- private String adminAddresses; private String accessToken; private int timeout; private Boolean enabled; 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 setTimeout(int timeout) { this.timeout = timeout; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } 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 { // valid enabled if (enabled!=null && !enabled) { logger.info(">>>>>>>>>>> xxl-job executor start fail, enabled:{}", enabled); return; } // init logpath XxlJobFileAppender.initLogPath(logPath); // init invoker, admin-client initAdminBizList(adminAddresses, accessToken, timeout); // 1、init JobLogFileCleanThread JobLogFileCleanThread.getInstance().start(logRetentionDays); // 2、init TriggerCallbackThread TriggerCallbackThread.getInstance().start(); // 3、init executor-server initEmbedServer(address, ip, port, appname, accessToken); } public void destroy(){ // 1、destroy executor-server stopEmbedServer(); // destroy jobThreadRepository if (!jobThreadRepository.isEmpty()) { // 1.1、elegant shutdown wait job finish try { TimeUnit.SECONDS.sleep(ELEGANT_SHUTDOWN_WAITING_SECONDS); } catch (Throwable e) { logger.error(e.getMessage(), e); } // 1.2、interupt all job-thread for (Map.Entry 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(); // 2、destroy JobLogFileCleanThread JobLogFileCleanThread.getInstance().toStop(); // 3、destroy TriggerCallbackThread TriggerCallbackThread.getInstance().toStop(); } // ---------------------- admin-client (rpc invoker) ---------------------- private static List adminBizList; private void initAdminBizList(String adminAddresses, String accessToken, int timeout) throws Exception { // valid if (StringTool.isBlank(adminAddresses)) { return; } // build adminBizList for (String address: adminAddresses.trim().split(",")) { if (StringTool.isBlank(address)) { continue; } // parse param String finalAddress = address.trim(); finalAddress = finalAddress.endsWith("/") ? (finalAddress + "api") : (finalAddress + "/api"); int finalTimeout = (timeout >=1 && timeout <= 10) ?timeout :3; // build AdminBiz adminBiz = HttpTool.createClient() .url(finalAddress) .timeout(finalTimeout * 1000) .header(Const.XXL_JOB_ACCESS_TOKEN, accessToken) .proxy(AdminBiz.class); // registry if (adminBizList == null) { adminBizList = new ArrayList(); } adminBizList.add(adminBiz); } } public static List 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: IPTool.getAvailablePort(9999); ip = StringTool.isNotBlank(ip) ? ip : IPTool.getIp(); // generate address if (StringTool.isBlank(address)) { // registry-address:default use address to registry , otherwise use ip:port if address is null String ip_port_address = IPTool.toAddressString(ip, port); address = "http://{ip_port}/".replace("{ip_port}", ip_port_address); } // accessToken if (StringTool.isBlank(accessToken)) { 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 jobHandlerRepository = new ConcurrentHashMap(); public static IJobHandler loadJobHandler(String name){ return jobHandlerRepository.get(name); } public static IJobHandler registryJobHandler(String name, IJobHandler jobHandler){ logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler); return jobHandlerRepository.put(name, jobHandler); } protected void registryJobHandler(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 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 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 registryJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod)); } // ---------------------- job thread repository ---------------------- private static ConcurrentMap jobThreadRepository = new ConcurrentHashMap(); 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); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java ================================================ package com.xxl.job.core.executor.impl; import com.xxl.job.core.executor.XxlJobExecutor; import com.xxl.job.core.handler.annotation.XxlJob; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; /** * 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 xxlJobBeanList = new ArrayList<>(); public List getXxlJobBeanList() { return xxlJobBeanList; } public void setXxlJobBeanList(List 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 xxlJobBeanList) { if (xxlJobBeanList==null || xxlJobBeanList.isEmpty()) { 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 registryJobHandler(xxlJob, bean, executeMethod); } } } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java ================================================ 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 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.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; 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.ArrayList; import java.util.List; 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); // ---------------------- field ---------------------- /** * excluded package, like "org.springframework"、"org.aaa,org.bbb" */ private String excludedPackage = "org.springframework.,spring."; public void setExcludedPackage(String excludedPackage) { this.excludedPackage = excludedPackage; } // ---------------------- start / stop ---------------------- /** * start */ @Override public void afterSingletonsInstantiated() { // scan JobHandler method scanJobHandlerMethod(applicationContext); // refresh GlueFactory GlueFactory.refreshInstance(1); // super start try { super.start(); } catch (Exception e) { throw new RuntimeException(e); } } /** * stop */ @Override public void destroy() { super.destroy(); } /** * init job handler from method * * @param applicationContext applicationContext */ private void scanJobHandlerMethod(ApplicationContext applicationContext) { // valid if (applicationContext == null) { return; } // 1、build excluded-package list List excludedPackageList = new ArrayList<>(); if (excludedPackage != null) { for (String excludedPackage : excludedPackage.split(",")) { if (!excludedPackage.trim().isEmpty()){ excludedPackageList.add(excludedPackage.trim()); } } } // 2、scan bean form jobhandler String[] beanNames = applicationContext.getBeanNamesForType(Object.class, false, false); // allowEagerInit=false, avoid early initialization for (String beanName : beanNames) { /** * 2.1、skip by BeanDefinition: * - skip excluded-package bean * - skip lazy-init bean */ if (applicationContext instanceof BeanDefinitionRegistry beanDefinitionRegistry) { // get BeanDefinition if (!beanDefinitionRegistry.containsBeanDefinition(beanName)) { continue; } BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(beanName); // skip excluded-package bean String beanClassName = beanDefinition.getBeanClassName(); if (isExcluded(excludedPackageList, beanClassName)) { logger.debug(">>>>>>>>>>> xxl-job bean-definition scan, skip excluded-package beanName:{}, beanClassName:{}", beanName, beanClassName); continue; } // skip lazy-init bean if (beanDefinition.isLazyInit()) { logger.debug(">>>>>>>>>>> xxl-job bean-definition scan, skip lazy-init beanName:{}", beanName); continue; } } /** * 2.2、skip by BeanDefinition Class * - skip beanClass is null * - skip method annotation(@XxlJob) is null */ Class beanClass = applicationContext.getType(beanName, false); if (beanClass == null) { logger.debug(">>>>>>>>>>> xxl-job bean-definition scan, skip beanClass-null beanName:{}", beanName); continue; } // filter method Map annotatedMethods = null; try { annotatedMethods = MethodIntrospector.selectMethods(beanClass, new MethodIntrospector.MetadataLookup() { @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[" + beanName + "].", ex); } if (annotatedMethods==null || annotatedMethods.isEmpty()) { continue; } // 2.3、scan + registry Jobhandler Object jobBean = applicationContext.getBean(beanName); for (Map.Entry jobMethodEntry : annotatedMethods.entrySet()) { Method jobMethod = jobMethodEntry.getKey(); XxlJob xxlJob = jobMethodEntry.getValue(); // regist registryJobHandler(xxlJob, jobBean, jobMethod); } } } /** * check bean if excluded * * @param excludedPackageList excludedPackageList * @param beanClassName beanClassName * @return true if excluded */ private boolean isExcluded(List excludedPackageList, String beanClassName) { // excludedPackageList is empty, no excluded if (excludedPackageList == null || excludedPackageList.isEmpty()) { return false; } // beanClassName is null, no excluded if (beanClassName == null) { return false; } // excludedPackageList match, excluded (not scan) for (String excludedPackage : excludedPackageList) { if (beanClassName.startsWith(excludedPackage)) { return true; } } return false; } // ---------------------- applicationContext ---------------------- private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { XxlJobSpringExecutor.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java ================================================ 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; } /** * refresh instance by type * * @param type 0-frameless, 1-spring; */ 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> 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 } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java ================================================ 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(Python3)", true, "python3", ".py"), GLUE_PYTHON2("GLUE(Python2)", true, "python", ".py"), GLUE_NODEJS("GLUE(Nodejs)", true, "node", ".js"), GLUE_POWERSHELL("GLUE(PowerShell)", true, "powershell", ".ps1"), GLUE_PHP("GLUE(PHP)", true, "php", ".php"); 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; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java ================================================ package com.xxl.job.core.glue.impl; import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import com.xxl.job.core.glue.GlueFactory; import jakarta.annotation.Resource; 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 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 (Exception e) { logger.error(e.getMessage(), e); } } } } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java ================================================ 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 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 } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java ================================================ //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(); // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java ================================================ 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 ""; } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java ================================================ 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(); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java ================================================ 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() +"]"; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java ================================================ 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) { for (File glueSrcFileItem : glueSrcFileList) { if (glueSrcFileItem.getName().startsWith(jobId +"_")) { glueSrcFileItem.delete(); } } } } } public long getGlueUpdatetime() { return glueUpdatetime; } @Override public void execute() throws Exception { // valid 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().getLogFileName(); // script params:0=param、1=分片序号、2=分片总数 String jobParam = XxlJobHelper.getJobParam(); String[] scriptParams = new String[3]; scriptParams[0] = jobParam!=null?jobParam:""; 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 ; } } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java ================================================ package com.xxl.job.core.log; import com.xxl.job.core.openapi.model.LogResult; import com.xxl.tool.core.DateTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.io.FileTool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.Date; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; /** * store trigger log in each log-file * * @author xuxueli 2016-3-12 19:25:12 */ public class XxlJobFileAppender { private static final Logger logger = LoggerFactory.getLogger(XxlJobFileAppender.class); /** * log base path * * strut like: * ---/ * ---/gluesource/10_1514171108000.js * ---/callbacklogs/xxl-job-callback-1761412677119.log * ---/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(File.separator).concat("gluesource"); private static String callbackLogPath = logBasePath.concat(File.separator).concat("callbacklogs"); public static void initLogPath(String logPath) throws IOException { // init if (StringTool.isNotBlank(logPath)) { logBasePath = logPath.trim(); } // mk base dir File logPathDir = new File(logBasePath); FileTool.createDirectories(logPathDir); logBasePath = logPathDir.getPath(); // mk glue dir File glueBaseDir = new File(logPathDir, "gluesource"); FileTool.createDirectories(glueBaseDir); glueSrcPath = glueBaseDir.getPath(); } public static String getLogPath() { return logBasePath; } public static String getGlueSrcPath() { return glueSrcPath; } public static String getCallbackLogPath() { return callbackLogPath; } /** * log filename, like "logPath/yyyy-MM-dd/9999.log" * * @param logId log id * @return log file name */ public static String makeLogFileName(Date triggerDate, long logId) { // "filePath/yyyy-MM-dd" File logFilePath = new File(getLogPath(), DateTool.formatDate(triggerDate)); try { FileTool.createDirectories(logFilePath); } catch (IOException e) { throw new RuntimeException("XxlJobFileAppender makeLogFileName error, logFilePath:"+ logFilePath.getPath(), e); } // filePath/yyyy-MM-dd/9999.log return logFilePath.getPath() .concat(File.separator) .concat(String.valueOf(logId)) .concat(".log"); } /** * append log * * @param logFileName log file name * @param appendLog append log */ public static void appendLog(String logFileName, String appendLog) { // valid if (StringTool.isBlank(logFileName) || appendLog == null) { return; } // append log try { FileTool.writeLines(logFileName, List.of(appendLog), true); } catch (IOException e) { throw new RuntimeException("XxlJobFileAppender appendLog error, logFileName:"+ logFileName, e); } } /** * support read log-file * * @param logFileName log file name * @param fromLineNum from line num * @return log content */ public static LogResult readLog(String logFileName, final int fromLineNum){ // valid if (StringTool.isBlank(logFileName)) { return new LogResult(fromLineNum, 0, "readLog fail, logFile not found", true); } if (!FileTool.exists(logFileName)) { return new LogResult(fromLineNum, 0, "readLog fail, logFile not exists", true); } // read data StringBuilder logContentBuilder = new StringBuilder(); // num: [from, to], start as 1 AtomicInteger toLineNum = new AtomicInteger(0); AtomicInteger currentLineNum = new AtomicInteger(0); /*int readLineCount = 0;*/ // do read try { FileTool.readLines(logFileName, new Consumer() { @Override public void accept(String line) { // refresh line num currentLineNum.incrementAndGet(); // valid if (currentLineNum.get() < fromLineNum) { return; } // Limit return less than 1000 rows per query request // todo /*if(++readLineCount >= 1000) { break; }*/ // collect line data toLineNum.set(currentLineNum.get()); logContentBuilder.append(line).append(System.lineSeparator()); // [from, to], start as 1 } }); } catch (IOException e) { logger.error("XxlJobFileAppender readLog error, logFileName:{}, fromLineNum:{}", logFileName, fromLineNum, e); } // result return new LogResult(fromLineNum, toLineNum.get(), logContentBuilder.toString(), false); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/AdminBiz.java ================================================ package com.xxl.job.core.openapi; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.openapi.model.RegistryRequest; import com.xxl.tool.response.Response; import java.util.List; /** * @author xuxueli 2017-07-27 21:52:49 */ public interface AdminBiz { // ---------------------- callback ---------------------- /** * callback * * @param callbackRequestList * @return */ public Response callback(List callbackRequestList); // ---------------------- registry ---------------------- /** * registry * * @param registryRequest * @return */ public Response registry(RegistryRequest registryRequest); /** * registry remove * * @param registryRequest * @return */ public Response registryRemove(RegistryRequest registryRequest); // ---------------------- biz (custome) ---------------------- // group、job ... manage } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/ExecutorBiz.java ================================================ package com.xxl.job.core.openapi; import com.xxl.job.core.openapi.model.*; import com.xxl.tool.response.Response; /** * Created by xuxueli on 17/3/1. */ public interface ExecutorBiz { /** * beat * @return response */ public Response beat(); /** * idle beat * * @param idleBeatRequest idleBeatRequest * @return response */ public Response idleBeat(IdleBeatRequest idleBeatRequest); /** * run * @param triggerRequest triggerRequest * @return response */ public Response run(TriggerRequest triggerRequest); /** * kill * @param killRequest killRequest * @return response */ public Response kill(KillRequest killRequest); /** * log * @param logRequest logRequest * @return response */ public Response log(LogRequest logRequest); } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/impl/ExecutorBizImpl.java ================================================ package com.xxl.job.core.openapi.impl; import com.xxl.job.core.context.XxlJobContext; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.model.*; import com.xxl.job.core.constant.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 com.xxl.tool.response.Response; 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 Response beat() { return Response.ofSuccess(); } @Override public Response idleBeat(IdleBeatRequest idleBeatRequest) { // isRunningOrHasQueue boolean isRunningOrHasQueue = false; JobThread jobThread = XxlJobExecutor.loadJobThread(idleBeatRequest.getJobId()); if (jobThread != null && jobThread.isRunningOrHasQueue()) { isRunningOrHasQueue = true; } if (isRunningOrHasQueue) { return Response.ofFail("job thread is running or has trigger queue."); } return Response.ofSuccess(); } @Override public Response run(TriggerRequest triggerRequest) { // load old:jobHandler + jobThread JobThread jobThread = XxlJobExecutor.loadJobThread(triggerRequest.getJobId()); IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null; String removeOldReason = null; // valid:jobHandler + jobThread GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerRequest.getGlueType()); if (GlueTypeEnum.BEAN == glueTypeEnum) { // new jobhandler IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerRequest.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 Response.of(XxlJobContext.HANDLE_CODE_FAIL, "job handler [" + triggerRequest.getExecutorHandler() + "] not found."); } } } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) { // valid old jobThread if (jobThread != null && !(jobThread.getHandler() instanceof GlueJobHandler && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()== triggerRequest.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(triggerRequest.getGlueSource()); jobHandler = new GlueJobHandler(originJobHandler, triggerRequest.getGlueUpdatetime()); } catch (Exception e) { logger.error(e.getMessage(), e); return Response.of(XxlJobContext.HANDLE_CODE_FAIL, e.getMessage()); } } } else if (glueTypeEnum!=null && glueTypeEnum.isScript()) { // valid old jobThread if (jobThread != null && !(jobThread.getHandler() instanceof ScriptJobHandler && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()== triggerRequest.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(triggerRequest.getJobId(), triggerRequest.getGlueUpdatetime(), triggerRequest.getGlueSource(), GlueTypeEnum.match(triggerRequest.getGlueType())); } } else { return Response.of(XxlJobContext.HANDLE_CODE_FAIL, "glueType[" + triggerRequest.getGlueType() + "] is not valid."); } // executor block strategy if (jobThread != null) { ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerRequest.getExecutorBlockStrategy(), null); if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) { // discard when running if (jobThread.isRunningOrHasQueue()) { return Response.of(XxlJobContext.HANDLE_CODE_FAIL, "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(triggerRequest.getJobId(), jobHandler, removeOldReason); } // push data to queue return jobThread.pushTriggerQueue(triggerRequest); } @Override public Response kill(KillRequest killRequest) { // kill handlerThread, and create new one JobThread jobThread = XxlJobExecutor.loadJobThread(killRequest.getJobId()); if (jobThread != null) { XxlJobExecutor.removeJobThread(killRequest.getJobId(), "scheduling center kill job."); return Response.ofSuccess(); } return Response.ofSuccess( "job thread already killed."); } @Override public Response log(LogRequest logRequest) { // log filename: logPath/yyyy-MM-dd/9999.log String logFileName = XxlJobFileAppender.makeLogFileName(new Date(logRequest.getLogDateTim()), logRequest.getLogId()); LogResult logResult = XxlJobFileAppender.readLog(logFileName, logRequest.getFromLineNum()); return Response.ofSuccess(logResult); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/CallbackRequest.java ================================================ package com.xxl.job.core.openapi.model; import java.io.Serializable; /** * Created by xuxueli on 17/3/2. */ public class CallbackRequest implements Serializable { private static final long serialVersionUID = 42L; private long logId; private long logDateTim; private int handleCode; private String handleMsg; public CallbackRequest(){} public CallbackRequest(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 + '\'' + '}'; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/IdleBeatRequest.java ================================================ package com.xxl.job.core.openapi.model; import java.io.Serializable; /** * @author xuxueli 2020-04-11 22:27 */ public class IdleBeatRequest implements Serializable { private static final long serialVersionUID = 42L; public IdleBeatRequest() { } public IdleBeatRequest(int jobId) { this.jobId = jobId; } private int jobId; public int getJobId() { return jobId; } public void setJobId(int jobId) { this.jobId = jobId; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/KillRequest.java ================================================ package com.xxl.job.core.openapi.model; import java.io.Serializable; /** * @author xuxueli 2020-04-11 22:27 */ public class KillRequest implements Serializable { private static final long serialVersionUID = 42L; public KillRequest() { } public KillRequest(int jobId) { this.jobId = jobId; } private int jobId; public int getJobId() { return jobId; } public void setJobId(int jobId) { this.jobId = jobId; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/LogRequest.java ================================================ package com.xxl.job.core.openapi.model; import java.io.Serializable; /** * @author xuxueli 2020-04-11 22:27 */ public class LogRequest implements Serializable { private static final long serialVersionUID = 42L; public LogRequest() { } public LogRequest(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; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/LogResult.java ================================================ package com.xxl.job.core.openapi.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; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/RegistryRequest.java ================================================ package com.xxl.job.core.openapi.model; import java.io.Serializable; /** * Created by xuxueli on 2017-05-10 20:22:42 */ public class RegistryRequest implements Serializable { private static final long serialVersionUID = 42L; private String registryGroup; private String registryKey; private String registryValue; public RegistryRequest(){} public RegistryRequest(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 + '\'' + '}'; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/openapi/model/TriggerRequest.java ================================================ package com.xxl.job.core.openapi.model; import java.io.Serializable; /** * Created by xuxueli on 16/7/22. */ public class TriggerRequest implements Serializable{ private static final long serialVersionUID = 42L; // job base info private int jobId; // job execute info private String executorHandler; private String executorParams; private String executorBlockStrategy; private int executorTimeout; // log info private long logId; private long logDateTime; // glue info private String glueType; private String glueSource; private long glueUpdatetime; // broadcast info 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 + '}'; } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java ================================================ package com.xxl.job.core.server; import com.xxl.job.core.constant.Const; import com.xxl.job.core.openapi.ExecutorBiz; import com.xxl.job.core.openapi.impl.ExecutorBizImpl; import com.xxl.job.core.openapi.model.*; import com.xxl.job.core.thread.ExecutorRegistryThread; import com.xxl.tool.error.ThrowableTool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.response.Response; 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(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() { @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 (Throwable e) { logger.error(">>>>>>>>>>> xxl-job remoting server error.", e); } finally { // stop try { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } catch (Throwable 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 *

      * Copy from : https://github.com/xuxueli/xxl-rpc * * @author xuxueli 2015-11-24 22:25:15 */ public static class EmbedHttpServerHandler extends SimpleChannelInboundHandler { 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(Const.XXL_JOB_ACCESS_TOKEN); // invoke bizThreadPool.execute(new Runnable() { @Override public void run() { // do invoke Object responseObj = dispatchRequest(httpMethod, uri, requestData, accessTokenReq); // to json String responseJson = GsonTool.toJson(responseObj); // write response writeResponse(ctx, keepAlive, responseJson); } }); } private Object dispatchRequest(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) { // valid if (HttpMethod.POST != httpMethod) { return Response.ofFail("invalid request, HttpMethod not support."); } if (uri == null || uri.trim().isEmpty()) { return Response.ofFail( "invalid request, uri-mapping empty."); } if (accessToken != null && !accessToken.trim().isEmpty() && !accessToken.equals(accessTokenReq)) { return Response.ofFail("The access token is wrong."); } // services mapping try { switch (uri) { case "/beat": return executorBiz.beat(); case "/idleBeat": IdleBeatRequest idleBeatParam = GsonTool.fromJson(requestData, IdleBeatRequest.class); return executorBiz.idleBeat(idleBeatParam); case "/run": TriggerRequest triggerParam = GsonTool.fromJson(requestData, TriggerRequest.class); return executorBiz.run(triggerParam); case "/kill": KillRequest killParam = GsonTool.fromJson(requestData, KillRequest.class); return executorBiz.kill(killParam); case "/log": LogRequest logParam = GsonTool.fromJson(requestData, LogRequest.class); return executorBiz.log(logParam); default: return Response.ofFail( "invalid request, uri-mapping(" + uri + ") not found."); } } catch (Throwable e) { logger.error(e.getMessage(), e); return Response.ofFail("request error:" + ThrowableTool.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(); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java ================================================ package com.xxl.job.core.thread; import com.xxl.job.core.constant.RegistType; import com.xxl.job.core.openapi.AdminBiz; import com.xxl.job.core.openapi.model.RegistryRequest; import com.xxl.job.core.constant.Const; import com.xxl.job.core.executor.XxlJobExecutor; import com.xxl.tool.response.Response; 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 { RegistryRequest registryParam = new RegistryRequest(RegistType.EXECUTOR.name(), appname, address); for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { try { Response registryResult = adminBiz.registry(registryParam); if (registryResult!=null && registryResult.isSuccess()) { registryResult = Response.ofSuccess(); 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 (Throwable e) { logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e); } } } catch (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } try { if (!toStop) { TimeUnit.SECONDS.sleep(Const.BEAT_TIMEOUT); } } catch (Throwable e) { if (!toStop) { logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage()); } } } // registry remove try { RegistryRequest registryParam = new RegistryRequest(RegistType.EXECUTOR.name(), appname, address); for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { try { Response registryResult = adminBiz.registryRemove(registryParam); if (registryResult!=null && registryResult.isSuccess()) { registryResult = Response.ofSuccess(); 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 (Throwable e) { if (!toStop) { logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e); } } } } catch (Throwable 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 (Throwable e) { logger.error(e.getMessage(), e); } } } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java ================================================ package com.xxl.job.core.thread; import com.xxl.job.core.log.XxlJobFileAppender; import com.xxl.tool.core.DateTool; import com.xxl.tool.io.FileTool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; 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; // effective only when logRetentionDays >= 3 } 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(); // clean expired logfile for (File childFile: childDirs) { // valid log-path: must be directory if (!childFile.isDirectory()) { continue; } // valid day log-path: like "---/2017-12-25/639.log" if (!childFile.getName().contains("-")) { continue; } // parse create-day of file-path Date logFileCreateDate = null; try { logFileCreateDate = DateTool.parseDate(childFile.getName()); } catch (Exception e) { logger.error(e.getMessage(), e); } if (logFileCreateDate == null) { continue; } // check expired Date expiredDate = DateTool.addDays(logFileCreateDate, logRetentionDays); if (todayDate.getTime() > expiredDate.getTime()) { // expired, remove all log of this day FileTool.delete(childFile); //FileUtil.deleteRecursively(childFile); } } } } catch (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } try { TimeUnit.DAYS.sleep(1); } catch (Throwable 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); } } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java ================================================ package com.xxl.job.core.thread; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.openapi.model.TriggerRequest; 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 com.xxl.tool.response.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Date; 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 final Logger logger = LoggerFactory.getLogger(JobThread.class); private int jobId; private IJobHandler handler; private LinkedBlockingQueue triggerQueue; private Set 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; // idle times public JobThread(int jobId, IJobHandler handler) { this.jobId = jobId; this.handler = handler; this.triggerQueue = new LinkedBlockingQueue(); //this.triggerLogIdSet = Collections.synchronizedSet(new HashSet()); this.triggerLogIdSet = ConcurrentHashMap.newKeySet(); // assign job thread name this.setName("xxl-job, JobThread-"+jobId+"-"+System.currentTimeMillis()); } public IJobHandler getHandler() { return handler; } /** * new trigger to queue */ public Response pushTriggerQueue(TriggerRequest triggerParam) { // avoid repeat if (!triggerLogIdSet.add(triggerParam.getLogId())) { logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId()); return Response.of(XxlJobContext.HANDLE_CODE_FAIL, "repeate trigger job, logId:" + triggerParam.getLogId()); } // push trigger queue triggerQueue.add(triggerParam); return Response.ofSuccess(); } /** * kill job thread */ public void toStop(String stopReason) { /** * Thread.interrupt只支持终止线程的阻塞状态(wait、join、sleep), * 在阻塞出抛出InterruptedException异常,但是并不会终止运行的线程本身; * 所以需要注意,此处彻底销毁本线程,需要通过共享变量方式; */ this.toStop = true; this.stopReason = stopReason; } /** * is running job */ 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++; TriggerRequest triggerParam = null; try { // to check toStop signal, we need cycle, so we cannot use queue.take(), instead 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(), triggerParam.getLogId(), triggerParam.getLogDateTime(), logFileName, triggerParam.getBroadcastIndex(), triggerParam.getBroadcastTotal()); // init job context XxlJobContext.setXxlJobContext(xxlJobContext); // execute XxlJobHelper.log("
      ----------- xxl-job job execute start -----------
      ----------- Param:" + xxlJobContext.getJobParam()); if (triggerParam.getExecutorTimeout() > 0) { // limit timeout Thread futureThread = null; try { FutureTask futureTask = new FutureTask(new Callable() { @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("
      ----------- 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("
      ----------- xxl-job job execute end(finish) -----------
      ----------- Result: handleCode=" + XxlJobContext.getXxlJobContext().getHandleCode() + ", handleMsg = " + XxlJobContext.getXxlJobContext().getHandleMsg() ); } else { if (idleTimes > 30) { if(triggerQueue.isEmpty()) { // avoid concurrent trigger causes jobId-lost XxlJobExecutor.removeJobThread(jobId, "excutor idle times over limit."); } } } } catch (Throwable e) { if (toStop) { XxlJobHelper.log("
      ----------- JobThread toStop, stopReason:" + stopReason); } // handle result StringWriter stringWriter = new StringWriter(); e.printStackTrace(new PrintWriter(stringWriter)); String errorMsg = stringWriter.toString(); XxlJobHelper.handleFail(errorMsg); XxlJobHelper.log("
      ----------- JobThread Exception:" + errorMsg + "
      ----------- xxl-job job execute end(error) -----------"); } finally { if(triggerParam != null) { // callback handler info if (!toStop) { // common TriggerCallbackThread.pushCallBack(new CallbackRequest( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.getXxlJobContext().getHandleCode(), XxlJobContext.getXxlJobContext().getHandleMsg() ) ); } else { // is killed TriggerCallbackThread.pushCallBack(new CallbackRequest( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.HANDLE_CODE_FAIL, stopReason + " [job running, killed]" ) ); } } } } // callback trigger request in queue while(triggerQueue !=null && !triggerQueue.isEmpty()){ TriggerRequest triggerParam = triggerQueue.poll(); if (triggerParam!=null) { // is killed TriggerCallbackThread.pushCallBack(new CallbackRequest( 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()); } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java ================================================ package com.xxl.job.core.thread; import com.xxl.job.core.openapi.AdminBiz; import com.xxl.job.core.openapi.model.CallbackRequest; import com.xxl.job.core.context.XxlJobContext; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.constant.Const; import com.xxl.job.core.executor.XxlJobExecutor; import com.xxl.job.core.log.XxlJobFileAppender; import com.xxl.tool.core.ArrayTool; import com.xxl.tool.core.CollectionTool; import com.xxl.tool.core.StringTool; import com.xxl.tool.crypto.Md5Tool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.io.FileTool; import com.xxl.tool.response.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; /** * Trigger Callback Thread * * Created by xuxueli on 16/7/22. */ public class TriggerCallbackThread { private static final Logger logger = LoggerFactory.getLogger(TriggerCallbackThread.class); private static final TriggerCallbackThread instance = new TriggerCallbackThread(); public static TriggerCallbackThread getInstance(){ return instance; } /** * job results callback queue */ private final LinkedBlockingQueue callBackQueue = new LinkedBlockingQueue<>(); public static void pushCallBack(CallbackRequest 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; } /** * trigger callback thread */ triggerCallbackThread = new Thread(new Runnable() { @Override public void run() { // normal callback while(!toStop){ try { CallbackRequest callback = getInstance().callBackQueue.take(); if (callback != null) { // collect callback data List callbackParamList = new ArrayList<>(); callbackParamList.add(callback); // add one element int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); // drainTo other all elements // do callback, will retry if error if (CollectionTool.isNotEmpty(callbackParamList)) { doCallback(callbackParamList); } } } catch (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } } // thead stop, callback lasttime try { // collect callback data List callbackParamList = new ArrayList<>(); int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); // do callback if (CollectionTool.isNotEmpty(callbackParamList)) { doCallback(callbackParamList); } } catch (Throwable 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(); /** * callback fail retry thread */ triggerRetryCallbackThread = new Thread(new Runnable() { @Override public void run() { while(!toStop){ try { retryFailCallbackFile(); } catch (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } try { TimeUnit.SECONDS.sleep(Const.BEAT_TIMEOUT); } catch (Throwable e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(">>>>>>>>>>> xxl-job, executor retry callback thread destroy."); } }); triggerRetryCallbackThread.setDaemon(true); triggerRetryCallbackThread.setName("xxl-job, executor TriggerRetryCallbackThread"); 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 (Throwable e) { logger.error(e.getMessage(), e); } } // stop retry, interrupt and wait if (triggerRetryCallbackThread != null) { triggerRetryCallbackThread.interrupt(); try { triggerRetryCallbackThread.join(); } catch (Throwable e) { logger.error(e.getMessage(), e); } } } /** * do callback, will retry if error * * @param callbackParamList callback param list */ private void doCallback(List callbackParamList){ boolean callbackRet = false; // callback, will retry if error for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { try { Response callbackResult = adminBiz.callback(callbackParamList); if (callbackResult!=null && callbackResult.isSuccess()) { callbackLog(callbackParamList, "
      ----------- xxl-job job callback finish."); callbackRet = true; break; } else { callbackLog(callbackParamList, "
      ----------- xxl-job job callback fail, callbackResult:" + callbackResult); } } catch (Throwable e) { callbackLog(callbackParamList, "
      ----------- xxl-job job callback error, errorMsg:" + e.getMessage()); } } if (!callbackRet) { appendFailCallbackFile(callbackParamList); } } /** * callback log */ private void callbackLog(List callbackParamList, String logContent){ for (CallbackRequest callbackParam: callbackParamList) { String logFileName = XxlJobFileAppender.makeLogFileName(new Date(callbackParam.getLogDateTim()), callbackParam.getLogId()); XxlJobContext.setXxlJobContext(new XxlJobContext( -1, null, -1, -1, logFileName, -1, -1)); XxlJobHelper.log(logContent); } } // ---------------------- fail-callback file ---------------------- /** * fail-callback file name */ private static final String failCallbackFileName = XxlJobFileAppender .getCallbackLogPath() .concat(File.separator) .concat("xxl-job-callback-{x}") .concat(".log"); /** * append fail-callback file * * @param callbackParamList callback param list */ private void appendFailCallbackFile(List callbackParamList) { // valid if (CollectionTool.isEmpty(callbackParamList)) { return; } // generate callback data String callbackData = GsonTool.toJson(callbackParamList); String callbackDataMd5 = Md5Tool.md5(callbackData); // create file String finalLogFileName = failCallbackFileName.replace("{x}", callbackDataMd5); // write callback log try { FileTool.writeString(finalLogFileName, callbackData); } catch (IOException e) { logger.error(">>>>>>>>>>> TriggerCallbackThread appendFailCallbackFile error, finalLogFileName:{}", finalLogFileName, e); } } /** * retry fail-callback file */ private void retryFailCallbackFile() { // valid File callbackLogPath = new File(XxlJobFileAppender.getCallbackLogPath()); if (!callbackLogPath.exists()) { return; } // valid file type: must be directory if (!FileTool.isDirectory(callbackLogPath)) { FileTool.delete(callbackLogPath); return; } // valid file in path: pass if empty if (ArrayTool.isEmpty(callbackLogPath.listFiles())) { return; } // load and clear file, do retry for (File callbackLogFile: callbackLogPath.listFiles()) { try { // load data String callbackData = FileTool.readString(callbackLogFile.getPath()); if (StringTool.isBlank(callbackData)) { FileTool.delete(callbackLogFile); continue; } // parse callback param List callbackParamList = GsonTool.fromJsonList(callbackData, CallbackRequest.class); FileTool.delete(callbackLogFile); // retry callback doCallback(callbackParamList); } catch (IOException e) { logger.error(">>>>>>>>>>> TriggerCallbackThread retryFailCallbackFile error, callbackLogFile:{}", callbackLogFile.getPath(), e); } } } } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java ================================================ package com.xxl.job.core.util; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.tool.core.ArrayTool; import com.xxl.tool.io.FileTool; import com.xxl.tool.io.IOTool; import java.io.FileOutputStream; import java.io.IOException; 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 script file name * @param scriptContent script content * @throws IOException exception */ public static void markScriptFile(String scriptFileName, String scriptContent) throws IOException { // make file: filePath/gluesource/666-123456789.py FileTool.writeString(scriptFileName, scriptContent); /*FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(scriptFileName); fileOutputStream.write(scriptContent.getBytes("UTF-8")); fileOutputStream.close(); } catch (Exception e) { throw e; }finally{ if(fileOutputStream != null){ fileOutputStream.close(); } }*/ } /** * 脚本执行,日志文件实时输出 * * @param command command * @param scriptFile script file * @param logFile log file * @param params params * @return exit code * @throws IOException exception */ public static int execToFile(String command, String scriptFile, String logFile, String... params) throws IOException { FileOutputStream fileOutputStream = null; Thread inputThread = null; Thread errorThread = null; Process process = null; try { // 1、build file OutputStream fileOutputStream = new FileOutputStream(logFile, true); // 2、build command List cmdarray = new ArrayList<>(); cmdarray.add(command); cmdarray.add(scriptFile); if (ArrayTool.isNotEmpty(params)) { for (String param:params) { cmdarray.add(param); } } String[] cmdarrayFinal = cmdarray.toArray(new String[0]); // 3、process:exec process = Runtime.getRuntime().exec(cmdarrayFinal); Process finalProcess = process; // 4、read script log: inputStream + errStream final FileOutputStream finalFileOutputStream = fileOutputStream; inputThread = new Thread(() -> { try { // 数据流Copy(Input自动关闭,Output不处理) IOTool.copy(finalProcess.getInputStream(), finalFileOutputStream, true, false); } catch (IOException e) { XxlJobHelper.log(e); } }); errorThread = new Thread(() -> { try { IOTool.copy(finalProcess.getErrorStream(), finalFileOutputStream, true, false); } catch (IOException e) { XxlJobHelper.log(e); } }); inputThread.start(); errorThread.start(); // 5、process:wait for result int exitValue = process.waitFor(); // exit code: 0=success, 1=error // 6、thread join, wait for log inputThread.join(); errorThread.join(); return exitValue; } catch (Exception e) { XxlJobHelper.log(e); return -1; } finally { // 7、close file OutputStream if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { XxlJobHelper.log(e); } } // 8、interrupt thread if (inputThread != null && inputThread.isAlive()) { inputThread.interrupt(); } if (errorThread != null && errorThread.isAlive()) { errorThread.interrupt(); } // 9、process destroy if (process != null) { process.destroy(); // process.destroyForcibly(); } } } /** * 脚本执行,日志文件实时输出 * * 优点:支持将目标数据实时输出到指定日志文件中去 * 缺点: * 标准输出和错误输出优先级固定,可能和脚本中顺序不一致 * Java无法实时获取 * * * * org.apache.commons * commons-exec * ${commons-exec.version} * * * @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); } } } }*/ } ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/AdminBizClient.java ================================================ //package com.xxl.job.core.openapi.client; // //import com.xxl.job.core.openapi.AdminBiz; //import com.xxl.job.core.openapi.model.HandleCallbackRequest; //import com.xxl.job.core.openapi.model.RegistryRequest; //import com.xxl.job.core.util.XxlJobRemotingUtil; //import com.xxl.tool.response.Response; // //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, int timeout) { // this.addressUrl = addressUrl; // this.accessToken = accessToken; // this.timeout = timeout; // // // valid // if (!this.addressUrl.endsWith("/")) { // this.addressUrl = this.addressUrl + "/"; // } // if (!(this.timeout >=1 && this.timeout <= 10)) { // this.timeout = 3; // } // } // // private String addressUrl ; // private String accessToken; // private int timeout; // // // @Override // public Response callback(List handleCallbackRequestList) { // return XxlJobRemotingUtil.postBody(addressUrl+"api/callback", accessToken, timeout, handleCallbackRequestList, String.class); // } // // @Override // public Response registry(RegistryRequest registryRequest) { // return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryRequest, String.class); // } // // @Override // public Response registryRemove(RegistryRequest registryRequest) { // return XxlJobRemotingUtil.postBody(addressUrl + "api/registryRemove", accessToken, timeout, registryRequest, String.class); // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/DateUtil.java ================================================ //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> dateFormatThreadLocal = new ThreadLocal>(); // private static DateFormat getDateFormat(String pattern) { // if (pattern==null || pattern.trim().length()==0) { // throw new IllegalArgumentException("pattern cannot be empty."); // } // // Map dateFormatMap = dateFormatThreadLocal.get(); // if(dateFormatMap!=null && dateFormatMap.containsKey(pattern)){ // return dateFormatMap.get(pattern); // } // // synchronized (dateFormatThreadLocal) { // if (dateFormatMap == null) { // dateFormatMap = new HashMap(); // } // 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(); // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/ExecutorBizClient.java ================================================ //package com.xxl.job.core.openapi.client; // //import com.xxl.job.core.openapi.ExecutorBiz; //import com.xxl.job.core.openapi.model.*; //import com.xxl.job.core.util.XxlJobRemotingUtil; //import com.xxl.tool.response.Response; // ///** // * admin api test // * // * @author xuxueli 2017-07-28 22:14:52 // */ //public class ExecutorBizClient implements ExecutorBiz { // // public ExecutorBizClient() { // } // public ExecutorBizClient(String addressUrl, String accessToken, int timeout) { // this.addressUrl = addressUrl; // this.accessToken = accessToken; // this.timeout = timeout; // // // valid // if (!this.addressUrl.endsWith("/")) { // this.addressUrl = this.addressUrl + "/"; // } // if (!(this.timeout >=1 && this.timeout <= 10)) { // this.timeout = 3; // } // } // // private String addressUrl ; // private String accessToken; // private int timeout; // // // @Override // public Response beat() { // return XxlJobRemotingUtil.postBody(addressUrl+"beat", accessToken, timeout, "", String.class); // } // // @Override // public Response idleBeat(IdleBeatRequest idleBeatRequest){ // return XxlJobRemotingUtil.postBody(addressUrl+"idleBeat", accessToken, timeout, idleBeatRequest, String.class); // } // // @Override // public Response run(TriggerRequest triggerRequest) { // return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerRequest, String.class); // } // // @Override // public Response kill(KillRequest killRequest) { // return XxlJobRemotingUtil.postBody(addressUrl + "kill", accessToken, timeout, killRequest, String.class); // } // // @Override // public Response log(LogRequest logRequest) { // return XxlJobRemotingUtil.postBody(addressUrl + "log", accessToken, timeout, logRequest, LogResult.class); // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/FileUtil.java ================================================ //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 loadFileLines(String fileName){ // // List 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; // }*/ // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/GsonTool.java ================================================ //package com.xxl.job.core.util; // //import com.google.gson.Gson; //import com.google.gson.GsonBuilder; //import com.google.gson.JsonElement; //import com.google.gson.reflect.TypeToken; // //import java.lang.reflect.Type; //import java.util.ArrayList; //import java.util.HashMap; // ///** // * gson tool (From https://github.com/xuxueli/xxl-tool ) // * // * @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").disableHtmlEscaping().create(); // } // // /** // * Object 转成 json // * // *

      //     *     String json = GsonTool.toJson(new Demo());
      //     * 
      // * // * @param src // * @return String // */ // public static String toJson(Object src) { // return gson.toJson(src); // } // // /** // * json 转成 特定的cls的Object // * // *
      //     *     Demo demo = GsonTool.fromJson(json, Demo.class);
      //     * 
      // * // * @param json // * @param classOfT // * @return // */ // public static T fromJson(String json, Class classOfT) { // return gson.fromJson(json, classOfT); // } // // /** // * json 转成 特定的 rawClass 的Object // * // *
      //     *     Response response = GsonTool.fromJson(json, Response.class, Demo.class);
      //     * 
      // * // * @param json // * @param classOfT // * @param argClassOfT // * @return // */ // /*public static T fromJson(String json, Class 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 转成 特定的 Type 的Object // * // * @param json // * @param typeOfT // * @return // * @param // */ // public static T fromJson(String json, Type typeOfT) { // return gson.fromJson(json, typeOfT); // } // // /** // * json 转成 特定的 Type 的Object // * // *
      //     *     Response response = GsonTool.fromJson(json, Response.class, Demo.class);
      //     * 
      // * // * @param json // * @param rawType // * @param typeArguments // * @return // */ // public static T fromJson(String json, Type rawType, Type... typeArguments) { // Type type = TypeToken.getParameterized(rawType, typeArguments).getType(); // return gson.fromJson(json, type); // } // // /** // * json 转成 特定的cls的 ArrayList // * // *
      //     *     List demoList = GsonTool.fromJsonList(json, Demo.class);
      //     * 
      // * // * @param json // * @param classOfT // * @return // */ // public static ArrayList fromJsonList(String json, Class classOfT) { // Type type = TypeToken.getParameterized(ArrayList.class, classOfT).getType(); // return gson.fromJson(json, type); // } // // /** // * json 转成 特定的cls的 HashMap // * // *
      //     *     HashMap map = GsonTool.fromJsonMap(json, String.class, Demo.class);
      //     * 
      // * // * @param json // * @param keyClass // * @param valueClass // * @return // * @param // * @param // */ // public static HashMap fromJsonMap(String json, Class keyClass, Class valueClass) { // Type type = TypeToken.getParameterized(HashMap.class, keyClass, valueClass).getType(); // return gson.fromJson(json, type); // } // // // --------------------------------- // // /** // * Object 转成 JsonElement // * // * @param src // * @return // */ // public static JsonElement toJsonElement(Object src) { // return gson.toJsonTree(src); // } // // /** // * JsonElement 转成 特定的cls的Object // * // * @param json // * @param classOfT // * @return // * @param // */ // public static T fromJsonElement(JsonElement json, Class classOfT) { // return gson.fromJson(json, classOfT); // } // // /** // * JsonElement 转成 特定的 rawClass 的Object // * // * @param json // * @param typeOfT // * @return // * @param // */ // public static T fromJsonElement(JsonElement json, Type typeOfT) { // return gson.fromJson(json, typeOfT); // } // // /** // * JsonElement 转成 特定的 Type 的 Object // * // * @param json // * @param rawType // * @param typeArguments // * @return // * @param // */ // public static T fromJsonElement(JsonElement json, Type rawType, Type... typeArguments) { // Type typeOfT = TypeToken.getParameterized(rawType, typeArguments).getType(); // return gson.fromJson(json, typeOfT); // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/IpUtil.java ================================================ //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 // *

      // * 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; // // 1、prefer filter NetworkInterface // try { // Enumeration 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 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); // } // // // 2、getLocalAddress // try { // localAddress = InetAddress.getLocalHost(); // InetAddress addressItem = toValidAddress(localAddress); // if (addressItem != null) { // return addressItem; // } // } 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}; // } // // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/JdkSerializeTool.java ================================================ //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 Object deserialize(byte[] bytes, Class clazz) { // ObjectInputStream ois = null; // ByteArrayInputStream bais = null; // try { // // 反序列化 // bais = new ByteArrayInputStream(bytes); // ois = new ObjectInputStream(bais); // return ois.readObject(); // } catch (Exception e) { // logger.error(e.getMessage(), e); // } finally { // try { // ois.close(); // bais.close(); // } catch (IOException e) { // logger.error(e.getMessage(), e); // } // } // return null; // } // // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/NetUtil.java ================================================ //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 available 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 - 1; // 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; // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/ReturnT.java ================================================ //package com.xxl.job.core.openapi.model; // //import java.io.Serializable; // ///** // * common return // * // * @author xuxueli 2015-12-4 16:32:31 // * @param // */ //public class ReturnT implements Serializable { // public static final long serialVersionUID = 42L; // // 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(int code, String msg, T content) { // this.code = code; // this.msg = msg; // 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 + // '}'; // } // // // // --------------------------- tool --------------------------- // // public static final int SUCCESS_CODE = 200; // public static final int FAIL_CODE = 500; // // /** // * is success // * // * @return // */ // public boolean isSuccess() { // return code == SUCCESS_CODE; // } // // public static ReturnT of(int code, String msg, T data) { // return new ReturnT(code, msg, data); // } // public static ReturnT of(int code, String msg) { // return new ReturnT(code, msg, null); // } // // public static ReturnT ofSuccess(T data) { // return new ReturnT(SUCCESS_CODE, "Success", data); // } // // public static ReturnT ofSuccess() { // return new ReturnT(SUCCESS_CODE, "Success", null); // } // // public static ReturnT ofFail(String msg) { // return new ReturnT(FAIL_CODE, msg, null); // } // public static ReturnT ofFail() { // return new ReturnT(FAIL_CODE, "Fail", null); // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/ShardingUtil.java ================================================ //package com.xxl.job.core.util; // ///** // * sharding vo // * @author xuxueli 2017-07-25 21:26:38 // */ //public class ShardingUtil { // // private static InheritableThreadLocal contextHolder = new InheritableThreadLocal(); // // 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(); // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/ThrowableUtil.java ================================================ //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; // } // //} ================================================ FILE: xxl-job-core/src/main/java/com/xxl/job/core/util/deprecated/XxlJobRemotingUtil.java ================================================ //package com.xxl.job.core.util; // //import com.xxl.tool.gson.GsonTool; //import com.xxl.tool.response.Response; //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; // ///** // * @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 by second // * @param requestObj // * @param returnTargClassOfT // * @return // */ // public static Response postBody(String url, String accessToken, int timeout, Object requestObj, Class returnTargClassOfT) { // HttpURLConnection connection = null; // BufferedReader bufferedReader = null; // DataOutputStream dataOutputStream = 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(timeout * 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().isEmpty()){ // connection.setRequestProperty(XXL_JOB_ACCESS_TOKEN, accessToken); // } // // // do connection // connection.connect(); // // // write requestBody // if (requestObj != null) { // String requestBody = GsonTool.toJson(requestObj); // // 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 Response.ofFail("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 { // Response returnT = GsonTool.fromJson(resultJson, Response.class, returnTargClassOfT); // return returnT; // } catch (Exception e) { // logger.error("xxl-job remoting (url="+url+") response content invalid("+ resultJson +").", e); // return Response.ofFail("xxl-job remoting (url="+url+") response content invalid("+ resultJson +")."); // } // // } catch (Exception e) { // logger.error(e.getMessage(), e); // return Response.ofFail("xxl-job remoting error("+ e.getMessage() +"), for url : " + url); // } finally { // try { // if (dataOutputStream != null) { // dataOutputStream.close(); // } // if (bufferedReader != null) { // bufferedReader.close(); // } // if (connection != null) { // connection.disconnect(); // } // } catch (Exception e2) { // logger.error(e2.getMessage(), e2); // } // } // } // //} ================================================ FILE: xxl-job-executor-samples/pom.xml ================================================ 4.0.0 com.xuxueli xxl-job 3.4.0-SNAPSHOT xxl-job-executor-samples pom xxl-job-executor-sample-frameless xxl-job-executor-sample-springboot xxl-job-executor-sample-springboot-ai true true ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml ================================================ 4.0.0 com.xuxueli xxl-job-executor-samples 3.4.0-SNAPSHOT xxl-job-executor-sample-frameless jar ${project.artifactId} Example executor project for spring boot. https://www.xuxueli.com/ org.slf4j slf4j-reload4j org.junit.jupiter junit-jupiter-engine test com.xuxueli xxl-job-core ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/XxlJobFramelessApplication.java ================================================ 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 XxlJobFramelessApplication { private static final Logger logger = LoggerFactory.getLogger(XxlJobFramelessApplication.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(); } } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java ================================================ 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 com.xxl.tool.core.PropTool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.Properties; /** * @author xuxueli 2018-10-31 19:05:43 */ public class FrameLessXxlJobConfig { private static final Logger logger = LoggerFactory.getLogger(FrameLessXxlJobConfig.class); private static final FrameLessXxlJobConfig instance = new FrameLessXxlJobConfig(); public static FrameLessXxlJobConfig getInstance() { return instance; } private XxlJobSimpleExecutor xxlJobExecutor = null; /** * init */ public void initXxlJobExecutor() { // load executor prop Properties xxlJobProp = PropTool.loadProp("xxl-job-executor.properties"); // init executor xxlJobExecutor = new XxlJobSimpleExecutor(); xxlJobExecutor.setAdminAddresses(xxlJobProp.getProperty("xxl.job.admin.addresses")); xxlJobExecutor.setAccessToken(xxlJobProp.getProperty("xxl.job.admin.accessToken")); xxlJobExecutor.setTimeout(Integer.valueOf(xxlJobProp.getProperty("xxl.job.admin.timeout"))); xxlJobExecutor.setEnabled(Boolean.valueOf(xxlJobProp.getProperty("xxl.job.executor.enabled"))); 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(); } } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java ================================================ package com.xxl.job.executor.sample.frameless.jobhandler; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.tool.core.StringTool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.http.HttpTool; import com.xxl.tool.http.http.HttpResponse; import com.xxl.tool.http.http.enums.ContentType; import com.xxl.tool.http.http.enums.Method; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Map; import java.util.Set; 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 final 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、命令行任务 * * 参数示例:"ls -a" 或者 "pwd" */ @XxlJob("commandJobHandler") public void commandJobHandler() throws Exception { String command = XxlJobHelper.getJobParam(); int exitValue = -1; BufferedReader bufferedReader = null; try { // valid if (command==null || command.trim().isEmpty()) { XxlJobHelper.handleFail("command empty."); return; } // command split String[] commandArray = command.split(" "); // command process ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.command(commandArray); 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任务 * * 参数示例: *

           *      // 1、简单示例:
           *      {
           *          "url": "http://www.baidu.com",
           *          "method": "get",
           *          "data": "hello world"
           *      }
           *
           *      // 2、完整参数示例:
           *      {
           *          "url": "http://www.baidu.com",
           *          "method": "POST",
           *          "contentType": "application/json",
           *          "headers": {
           *              "header01": "value01"
           *          },
           *          "cookies": {
           *              "cookie01": "value01"
           *          },
           *          "timeout": 3000,
           *          "data": "request body data",
           *          "form": {
           *              "key01": "value01"
           *          },
           *          "auth": "auth data"
           *      }
           *  
      */ @XxlJob("httpJobHandler") public void httpJobHandler() throws Exception { // param data String param = XxlJobHelper.getJobParam(); if (param==null || param.trim().isEmpty()) { XxlJobHelper.log("param["+ param +"] invalid."); XxlJobHelper.handleFail(); return; } // param parse HttpJobParam httpJobParam = null; try { httpJobParam = GsonTool.fromJson(param, HttpJobParam.class); } catch (Exception e) { XxlJobHelper.log(new RuntimeException("HttpJobParam parse error", e)); XxlJobHelper.handleFail(); return; } // param valid if (httpJobParam == null) { XxlJobHelper.log("param parse fail."); XxlJobHelper.handleFail(); return; } if (StringTool.isBlank(httpJobParam.getUrl())) { XxlJobHelper.log("url["+ httpJobParam.getUrl() +"] invalid."); XxlJobHelper.handleFail(); return; } if (!isValidDomain(httpJobParam.getUrl())) { XxlJobHelper.log("url["+ httpJobParam.getUrl() +"] not allowed."); XxlJobHelper.handleFail(); return; } Method method = Method.POST; if (StringTool.isNotBlank(httpJobParam.getMethod())) { Method methodParam = Method.valueOf(httpJobParam.getMethod().toUpperCase()); if (methodParam == null) { XxlJobHelper.log("method["+ httpJobParam.getMethod() +"] invalid."); XxlJobHelper.handleFail(); return; } method = methodParam; } ContentType contentType = ContentType.JSON; if (StringTool.isNotBlank(httpJobParam.getContentType())) { for (ContentType contentTypeParam : ContentType.values()) { if (contentTypeParam.getValue().equals(httpJobParam.getContentType())) { contentType = contentTypeParam; break; } } } if (httpJobParam.getTimeout() <= 0) { httpJobParam.setTimeout(3000); } // do request try { HttpResponse httpResponse = HttpTool.createRequest() .url(httpJobParam.getUrl()) .method(method) .contentType(contentType) .header(httpJobParam.getHeaders()) .cookie(httpJobParam.getCookies()) .body(httpJobParam.getData()) .form(httpJobParam.getForm()) .auth(httpJobParam.getAuth()) .execute(); XxlJobHelper.log("StatusCode: " + httpResponse.statusCode()); XxlJobHelper.log("Response:
      " + httpResponse.response()); } catch (Exception e) { XxlJobHelper.log(e); XxlJobHelper.handleFail(); } } /** * domain white-list, for httpJobHandler */ private static Set DOMAIN_WHITE_LIST = Set.of( "http://www.baidu.com", "http://cn.bing.com" ); /** * valid if domain is in white-list */ private boolean isValidDomain(String url) { if (url == null || DOMAIN_WHITE_LIST.isEmpty()) { return false; } for (String prefix : DOMAIN_WHITE_LIST) { if (url.startsWith(prefix)) { return true; } } return false; } /*public static void main(String[] args) { HttpJobParam httpJobParam = new HttpJobParam(); httpJobParam.setUrl("http://www.baidu.com"); httpJobParam.setMethod(Method.POST.name()); httpJobParam.setContentType(ContentType.JSON.getValue()); httpJobParam.setHeaders(Map.of("header01", "value01")); httpJobParam.setCookies(Map.of("cookie01", "value01")); httpJobParam.setTimeout(3000); httpJobParam.setData("request body data"); httpJobParam.setForm(Map.of("form01", "value01")); httpJobParam.setAuth("auth data"); logger.info(GsonTool.toJson(httpJobParam)); }*/ /** * http job param */ private static class HttpJobParam{ private String url; // 请求 Url private String method; // Method private String contentType; // Content-Type private Map headers; // 存储请求头 private Map cookies; // Cookie(需要格式转换) private int timeout; // 请求超时时间 private String data; // 存储请求体 private Map form; // 存储表单数据 private String auth; // 鉴权信息 public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } public Map getHeaders() { return headers; } public void setHeaders(Map headers) { this.headers = headers; } public Map getCookies() { return cookies; } public void setCookies(Map cookies) { this.cookies = cookies; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getData() { return data; } public void setData(String data) { this.data = data; } public Map getForm() { return form; } public void setForm(Map form) { this.form = form; } public String getAuth() { return auth; } public void setAuth(String auth) { this.auth = auth; } } /** * 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"); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml ================================================ ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties ================================================ ### 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.admin.accessToken=default_token ### xxl-job timeout by second, default 3s xxl.job.admin.timeout=3 ### xxl-job executor enable, default true xxl.job.executor.enabled=true ### 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 ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java ================================================ package com.xxl.job.executor.sample.frameless.test; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.platform.commons.annotation.Testable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Testable public class FramelessApplicationTest { private static final Logger logger = LoggerFactory.getLogger(FramelessApplicationTest.class); @Test @DisplayName("test1") public void test1(){ logger.info("111"); Assertions.assertNull( null); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile ================================================ # base image FROM openjdk:21-jdk-slim # maintainer MAINTAINER xuxueli # set params ENV PARAMS="" # set timezone ENV TZ=PRC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # copy jar ADD target/xxl-job-executor-sample-springboot-*.jar /app.jar # command # log home: -e LOG_HOME=/data/applogs # jvm options: -e JAVA_OPTS="-Xms128m -Xmx128m" # app params: -e PARAMS="--server.port=8080" ENTRYPOINT ["sh","-c","java ${LOG_HOME:+-DLOG_HOME=$LOG_HOME} -jar $JAVA_OPTS /app.jar $PARAMS"] ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml ================================================ 4.0.0 com.xuxueli xxl-job-executor-samples 3.4.0-SNAPSHOT xxl-job-executor-sample-springboot jar ${project.artifactId} Example executor project for spring boot. https://www.xuxueli.com/ org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.xuxueli xxl-job-core org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} repackage ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java ================================================ 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); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java ================================================ package com.xxl.job.executor.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 static final Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); @Value("${xxl.job.admin.addresses}") private String adminAddresses; @Value("${xxl.job.admin.accessToken}") private String accessToken; @Value("${xxl.job.admin.timeout}") private int timeout; @Value("${xxl.job.executor.enabled}") private Boolean enabled; @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; @Value("${xxl.job.executor.excludedpackage}") private String excludedPackage; @Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info(">>>>>>>>>>> xxl-job config init."); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setTimeout(timeout); xxlJobSpringExecutor.setEnabled(enabled); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); xxlJobSpringExecutor.setExcludedPackage(excludedPackage); return xxlJobSpringExecutor; } /** * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP; * * 1、引入依赖: * * org.springframework.cloud * spring-cloud-commons * ${version} * * * 2、配置文件,或者容器启动变量 * spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.' * * 3、获取IP * String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); */ } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/controller/IndexController.java ================================================ //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."; // } // //} ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/jobhandler/SampleXxlJob.java ================================================ package com.xxl.job.executor.jobhandler; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.tool.core.StringTool; import com.xxl.tool.json.GsonTool; import com.xxl.tool.http.HttpTool; import com.xxl.tool.http.http.HttpResponse; import com.xxl.tool.http.http.enums.ContentType; import com.xxl.tool.http.http.enums.Method; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Map; import java.util.Set; 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 final 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、命令行任务 * * 参数示例:"ls -a" 或者 "pwd" */ @XxlJob("commandJobHandler") public void commandJobHandler() throws Exception { String command = XxlJobHelper.getJobParam(); int exitValue = -1; BufferedReader bufferedReader = null; try { // valid if (command==null || command.trim().length()==0) { XxlJobHelper.handleFail("command empty."); return; } // command split String[] commandArray = command.split(" "); // command process ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.command(commandArray); 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任务 * * 参数示例: *
           *      // 1、简单示例:
           *      {
           *          "url": "http://www.baidu.com",
           *          "method": "get",
           *          "data": "hello world"
           *      }
           *
           *      // 2、完整参数示例:
           *      {
           *          "url": "http://www.baidu.com",
           *          "method": "POST",
           *          "contentType": "application/json",
           *          "headers": {
           *              "header01": "value01"
           *          },
           *          "cookies": {
           *              "cookie01": "value01"
           *          },
           *          "timeout": 3000,
           *          "data": "request body data",
           *          "form": {
           *              "key01": "value01"
           *          },
           *          "auth": "auth data"
           *      }
           *  
      */ @XxlJob("httpJobHandler") public void httpJobHandler() throws Exception { // param data String param = XxlJobHelper.getJobParam(); if (param==null || param.trim().isEmpty()) { XxlJobHelper.log("param["+ param +"] invalid."); XxlJobHelper.handleFail(); return; } // param parse HttpJobParam httpJobParam = null; try { httpJobParam = GsonTool.fromJson(param, HttpJobParam.class); } catch (Exception e) { XxlJobHelper.log(new RuntimeException("HttpJobParam parse error", e)); XxlJobHelper.handleFail(); return; } // param valid if (httpJobParam == null) { XxlJobHelper.log("param parse fail."); XxlJobHelper.handleFail(); return; } if (StringTool.isBlank(httpJobParam.getUrl())) { XxlJobHelper.log("url["+ httpJobParam.getUrl() +"] invalid."); XxlJobHelper.handleFail(); return; } if (!isValidDomain(httpJobParam.getUrl())) { XxlJobHelper.log("url["+ httpJobParam.getUrl() +"] not allowed."); XxlJobHelper.handleFail(); return; } Method method = Method.POST; if (StringTool.isNotBlank(httpJobParam.getMethod())) { Method methodParam = Method.valueOf(httpJobParam.getMethod().toUpperCase()); if (methodParam == null) { XxlJobHelper.log("method["+ httpJobParam.getMethod() +"] invalid."); XxlJobHelper.handleFail(); return; } method = methodParam; } ContentType contentType = ContentType.JSON; if (StringTool.isNotBlank(httpJobParam.getContentType())) { for (ContentType contentTypeParam : ContentType.values()) { if (contentTypeParam.getValue().equals(httpJobParam.getContentType())) { contentType = contentTypeParam; break; } } } if (httpJobParam.getTimeout() <= 0) { httpJobParam.setTimeout(3000); } // do request try { HttpResponse httpResponse = HttpTool.createRequest() .url(httpJobParam.getUrl()) .method(method) .contentType(contentType) .header(httpJobParam.getHeaders()) .cookie(httpJobParam.getCookies()) .body(httpJobParam.getData()) .form(httpJobParam.getForm()) .auth(httpJobParam.getAuth()) .execute(); XxlJobHelper.log("StatusCode: " + httpResponse.statusCode()); XxlJobHelper.log("Response:
      " + httpResponse.response()); } catch (Exception e) { XxlJobHelper.log(e); XxlJobHelper.handleFail(); } } /** * domain white-list, for httpJobHandler */ private static Set DOMAIN_WHITE_LIST = Set.of( "http://www.baidu.com", "http://cn.bing.com" ); /** * valid if domain is in white-list */ private boolean isValidDomain(String url) { if (url == null || DOMAIN_WHITE_LIST.isEmpty()) { return false; } for (String prefix : DOMAIN_WHITE_LIST) { if (url.startsWith(prefix)) { return true; } } return false; } /*public static void main(String[] args) { HttpJobParam httpJobParam = new HttpJobParam(); httpJobParam.setUrl("http://www.baidu.com"); httpJobParam.setMethod(Method.POST.name()); httpJobParam.setContentType(ContentType.JSON.getValue()); httpJobParam.setHeaders(Map.of("header01", "value01")); httpJobParam.setCookies(Map.of("cookie01", "value01")); httpJobParam.setTimeout(3000); httpJobParam.setData("request body data"); httpJobParam.setForm(Map.of("form01", "value01")); httpJobParam.setAuth("auth data"); logger.info(GsonTool.toJson(httpJobParam)); }*/ /** * http job param */ private static class HttpJobParam{ private String url; // 请求 Url private String method; // Method private String contentType; // Content-Type private Map headers; // 存储请求头 private Map cookies; // Cookie(需要格式转换) private int timeout; // 请求超时时间 private String data; // 存储请求体 private Map form; // 存储表单数据 private String auth; // 鉴权信息 public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } public Map getHeaders() { return headers; } public void setHeaders(Map headers) { this.headers = headers; } public Map getCookies() { return cookies; } public void setCookies(Map cookies) { this.cookies = cookies; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getData() { return data; } public void setData(String data) { this.data = data; } public Map getForm() { return form; } public void setForm(Map form) { this.form = form; } public String getAuth() { return auth; } public void setAuth(String auth) { this.auth = auth; } } /** * 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"); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties ================================================ # 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.admin.accessToken=default_token ### xxl-job timeout by second, default 3s xxl.job.admin.timeout=3 ### xxl-job executor enable, default true xxl.job.executor.enabled=true ### 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 ### xxl-job executor excluded package, will skip scan job. such as "org.package01" or "org.package01,org.package02" xxl.job.executor.excludedpackage= ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml ================================================ logback %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ${log.path} ${log.path}.%d{yyyy-MM-dd}.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java ================================================ 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); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/Dockerfile ================================================ # base image FROM openjdk:21-jdk-slim # maintainer MAINTAINER xuxueli # set params ENV PARAMS="" # set timezone ENV TZ=PRC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # copy jar ADD target/xxl-job-executor-sample-springboot-ai*.jar /app.jar # command # log home: -e LOG_HOME=/data/applogs # jvm options: -e JAVA_OPTS="-Xms128m -Xmx128m" # app params: -e PARAMS="--server.port=8080" ENTRYPOINT ["sh","-c","java ${LOG_HOME:+-DLOG_HOME=$LOG_HOME} -jar $JAVA_OPTS /app.jar $PARAMS"] ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/pom.xml ================================================ 4.0.0 com.xuxueli xxl-job-executor-samples 3.4.0-SNAPSHOT xxl-job-executor-sample-springboot-ai jar ${project.artifactId} Example executor project for spring boot. https://www.xuxueli.com/ org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.xuxueli xxl-job-core org.springframework.ai spring-ai-starter-model-ollama io.github.imfangs dify-java-client org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} repackage ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/XxlJobAIExecutorApplication.java ================================================ 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 XxlJobAIExecutorApplication { public static void main(String[] args) { SpringApplication.run(XxlJobAIExecutorApplication.class, args); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java ================================================ package com.xxl.job.executor.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 static final Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); @Value("${xxl.job.admin.addresses}") private String adminAddresses; @Value("${xxl.job.admin.accessToken}") private String accessToken; @Value("${xxl.job.admin.timeout}") private int timeout; @Value("${xxl.job.executor.enabled}") private Boolean enabled; @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; @Value("${xxl.job.executor.excludedpackage}") private String excludedPackage; @Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info(">>>>>>>>>>> xxl-job config init."); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setTimeout(timeout); xxlJobSpringExecutor.setEnabled(enabled); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); xxlJobSpringExecutor.setExcludedPackage(excludedPackage); return xxlJobSpringExecutor; } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/controller/IndexController.java ================================================ package com.xxl.job.executor.controller;//package com.xxl.job.executor.mvc.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.github.imfangs.dify.client.DifyClientFactory; import io.github.imfangs.dify.client.DifyWorkflowClient; import io.github.imfangs.dify.client.callback.WorkflowStreamCallback; import io.github.imfangs.dify.client.enums.ResponseMode; import io.github.imfangs.dify.client.event.*; import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest; import io.github.imfangs.dify.client.model.workflow.WorkflowRunResponse; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; @Controller @EnableAutoConfiguration public class IndexController { private static Logger logger = LoggerFactory.getLogger(IndexController.class); @RequestMapping("/") @ResponseBody String index() { return "xxl job ai executor running."; } // --------------------------------- ollama chat --------------------------------- @Resource private OllamaChatModel ollamaChatModel; private String prompt = "你好,你是一个研发工程师,擅长解决技术类问题。"; private String modle = "qwen3:0.6b"; /** * ChatClient 简单调用 */ @GetMapping("/chat/simple") @ResponseBody public String simpleChat(@RequestParam(value = "input", required = false, defaultValue = "介绍你自己") String input) { // build chat-client ChatClient ollamaChatClient = ChatClient .builder(ollamaChatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()) // add memory .defaultAdvisors(SimpleLoggerAdvisor.builder().build()) // add logger .defaultOptions(OllamaChatOptions.builder().model(modle).build()) // assign model .build(); // call ollama String response = ollamaChatClient .prompt(prompt) .user(input) .call() .content(); logger.info("result: " + response); return response; } /** * ChatClient 流式调用 */ @GetMapping("/chat/stream") public Flux streamChat(HttpServletResponse response, @RequestParam(value = "input", required = false, defaultValue = "介绍你自己") String input) { response.setCharacterEncoding("UTF-8"); // build chat-client ChatClient ollamaChatClient = ChatClient .builder(ollamaChatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()) .defaultAdvisors(SimpleLoggerAdvisor.builder().build()) .defaultOptions(OllamaChatOptions.builder().model(modle).build()) .build(); // call ollama return ollamaChatClient .prompt(prompt) .user(input) .stream() .content(); } // --------------------------------- dify workflow --------------------------------- // dify config sample private final String apiKey = "app-46gHBiqUb5jqAHl9TDWwnRZ8"; private final String baseUrl = "http://localhost/v1"; @GetMapping("/dify/simple") @ResponseBody public String difySimple(@RequestParam(required = false, value = "input") String input) throws Exception { Map inputs = new HashMap<>(); inputs.put("input", input); // request WorkflowRunRequest request = WorkflowRunRequest.builder() .inputs(inputs) .responseMode(ResponseMode.BLOCKING) .user("user-123") .build(); // invoke DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(baseUrl, apiKey); WorkflowRunResponse response = workflowClient.runWorkflow(request); // response return write2Json(response.getData().getOutputs()); } private String write2Json(Object obj) { if (obj == null) { return "null"; } try { return new ObjectMapper().writeValueAsString(obj); } catch (JsonProcessingException e) { return obj.toString(); } } @GetMapping( "/dify/stream") public Flux difyStream(@RequestParam(required = false, value = "input") String input) { Map inputs = new HashMap<>(); inputs.put("input", input); // request WorkflowRunRequest request = WorkflowRunRequest.builder() .inputs(inputs) .responseMode(ResponseMode.STREAMING) .user("user-123") .build(); // invoke DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(baseUrl, apiKey); return Flux.create(new Consumer>() { @Override public void accept(FluxSink sink) { try { workflowClient.runWorkflowStream(request, new WorkflowStreamCallback() { @Override public void onWorkflowStarted(WorkflowStartedEvent event) { sink.next("工作流开始: " + write2Json(event.getData())); } @Override public void onNodeStarted(NodeStartedEvent event) { sink.next("节点开始: " + write2Json(event.getData())); } @Override public void onNodeFinished(NodeFinishedEvent event) { sink.next("节点完成: " + write2Json(event.getData().getOutputs())); } @Override public void onWorkflowFinished(WorkflowFinishedEvent event) { sink.next("工作流完成: " + write2Json(event.getData().getOutputs())); sink.complete(); } @Override public void onError(ErrorEvent event) { sink.error(new RuntimeException(event.getMessage())); } @Override public void onException(Throwable throwable) { sink.error(throwable); } }); } catch (Exception e) { throw new RuntimeException(e); } } }); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java ================================================ package com.xxl.job.executor.jobhandler; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.tool.json.GsonTool; import io.github.imfangs.dify.client.DifyClientFactory; import io.github.imfangs.dify.client.DifyWorkflowClient; import io.github.imfangs.dify.client.enums.ResponseMode; import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest; import io.github.imfangs.dify.client.model.workflow.WorkflowRunResponse; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * AI 任务开发示例 * * @author xuxueli 2025-04-06 */ @Component public class AIXxlJob { // --------------------------------- ollama chat --------------------------------- @Resource private OllamaChatModel ollamaChatModel; /** * 1、ollama Chat任务 * * 参数示例:格式见 OllamaParam *
           *      {
           *          "input": "{输入信息,必填信息}",
           *          "prompt": "{模型prompt,可选信息}"
           *      }
           *  
      */ @XxlJob("ollamaJobHandler") public void ollamaJobHandler() { // param String param = XxlJobHelper.getJobParam(); if (param==null || param.trim().isEmpty()) { XxlJobHelper.log("param is empty."); XxlJobHelper.handleFail(); return; } // ollama param OllamaParam ollamaParam = null; try { ollamaParam = GsonTool.fromJson(param, OllamaParam.class); if (ollamaParam.getPrompt()==null || ollamaParam.getPrompt().isBlank()) { ollamaParam.setPrompt("你是一个研发工程师,擅长解决技术类问题。"); } if (ollamaParam.getInput() == null || ollamaParam.getInput().isBlank()) { XxlJobHelper.log("input is empty."); XxlJobHelper.handleFail(); return; } if (ollamaParam.getModel()==null || ollamaParam.getModel().isBlank()) { ollamaParam.setModel("qwen3:0.6b"); } } catch (Exception e) { XxlJobHelper.log(new RuntimeException("OllamaParam parse error", e)); XxlJobHelper.handleFail(); return; } // input XxlJobHelper.log("

      【Input】: " + ollamaParam.getInput()+ "

      "); // build chat-client ChatClient ollamaChatClient = ChatClient .builder(ollamaChatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()) .defaultAdvisors(SimpleLoggerAdvisor.builder().build()) .defaultOptions(OllamaChatOptions.builder().model(ollamaParam.getModel()).build()) .build(); // call ollama String response = ollamaChatClient .prompt(ollamaParam.getPrompt()) .user(ollamaParam.getInput()) .call() .content(); XxlJobHelper.log("

      【Output】: " + response + "

      "); } private static class OllamaParam{ private String input; private String prompt; private String model; public String getInput() { return input; } public void setInput(String input) { this.input = input; } public String getPrompt() { return prompt; } public void setPrompt(String prompt) { this.prompt = prompt; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } } // --------------------------------- dify workflow --------------------------------- /** * 2、dify Workflow任务 * * 参数示例:格式见 DifyParam *
           *      {
           *          "inputs":{                      // inputs 为dify工作流任务参数;参数不固定,结合各自 workflow 自行定义。
           *              "input":"{用户输入信息}"      // 该参数为示例变量,需要 workflow 的“开始”节点 自定义参数 “input”,可自行调整或删除。
           *          },
           *          "user": "{用户标识,选填}"
           *      }
           *  
      */ @XxlJob("difyWorkflowJobHandler") public void difyWorkflowJobHandler() throws Exception { // param String param = XxlJobHelper.getJobParam(); if (param==null || param.trim().isEmpty()) { XxlJobHelper.log("param is empty."); XxlJobHelper.handleFail(); return; } // param parse DifyParam difyParam; try { difyParam =GsonTool.fromJson(param, DifyParam.class); if (difyParam.getInputs() == null) { difyParam.setInputs(new HashMap<>()); } if (difyParam.getUser() == null) { difyParam.setUser("xxl-job"); } if (difyParam.getBaseUrl()==null || difyParam.getApiKey()==null) { XxlJobHelper.log("baseUrl or apiKey invalid."); XxlJobHelper.handleFail(); return; } } catch (Exception e) { XxlJobHelper.log(new RuntimeException("DifyParam parse error", e)); XxlJobHelper.handleFail(); return; } // dify param XxlJobHelper.log("

      【inputs】: " + difyParam.getInputs() + "

      "); // dify request WorkflowRunRequest request = WorkflowRunRequest.builder() .inputs(difyParam.getInputs()) .responseMode(ResponseMode.BLOCKING) .user(difyParam.getUser()) .build(); // dify invoke DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(difyParam.getBaseUrl(), difyParam.getApiKey()); WorkflowRunResponse response = workflowClient.runWorkflow(request); // response XxlJobHelper.log("

      【Output】: " + response.getData().getOutputs()+ "

      "); } private static class DifyParam{ /** * dify input, 允许传入 Dify App 定义的各变量值 */ private Map inputs; /** * dify user */ private String user; /** * dify baseUrl */ private String baseUrl; /** * dify apiKey */ private String apiKey; public Map getInputs() { return inputs; } public void setInputs(Map inputs) { this.inputs = inputs; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties ================================================ # web port server.port=8082 # no web #spring.main.web-environment=false server.servlet.encoding.force=true server.servlet.encoding.charset=UTF-8 # 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.admin.accessToken=default_token ### xxl-job timeout by second, default 3s xxl.job.admin.timeout=3 ### xxl-job executor enable, default true xxl.job.executor.enabled=true ### xxl-job executor appname xxl.job.executor.appname=xxl-job-executor-sample-ai ### 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=9997 ### 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 ### xxl-job executor excluded package, will skip scan job. such as "org.package01" or "org.package01,org.package02" xxl.job.executor.excludedpackage= ### ollama spring.ai.model.chat=ollama ### ollama url spring.ai.ollama.base-url=http://localhost:11434 ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/logback.xml ================================================ logback %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ${log.path} ${log.path}.%d{yyyy-MM-dd}.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/BaseTests.java ================================================ package com.xxl.job.executor.test; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class BaseTests { @Test public void test() { System.out.println(11); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/dify/DifyTest.java ================================================ package com.xxl.job.executor.test.dify; import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import io.github.imfangs.dify.client.DifyClientFactory; import io.github.imfangs.dify.client.DifyWorkflowClient; import io.github.imfangs.dify.client.enums.ResponseMode; import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest; import io.github.imfangs.dify.client.model.workflow.WorkflowRunResponse; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.util.Map; @SpringBootTest public class DifyTest { private static final Logger logger = LoggerFactory.getLogger(DifyTest.class); // ignore @MockitoBean private XxlJobSpringExecutor xxlJobSpringExecutor; @Test public void test() throws Exception { String baseUrl = "https://xx.ai"; String apiKey = "xx"; String user = "zhangsan"; Map inputs = Map.of( "input", "请写一个java程序,实现一个方法,输入一个字符串,返回字符串的长度。" ); // dify request WorkflowRunRequest request = WorkflowRunRequest.builder() .inputs(inputs) .responseMode(ResponseMode.BLOCKING) .user(user) .build(); // dify invoke DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(baseUrl, apiKey); WorkflowRunResponse response = workflowClient.runWorkflow(request); // response logger.info("input: " + inputs); logger.info("output: " + response.getData().getOutputs()); } } ================================================ FILE: xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/ollama/OllamaTest.java ================================================ package com.xxl.job.executor.test.ollama; import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; import reactor.core.publisher.Flux; import java.util.concurrent.TimeUnit; @SpringBootTest public class OllamaTest { private static final Logger logger = LoggerFactory.getLogger(OllamaTest.class); // ignore @MockitoBean private XxlJobSpringExecutor xxlJobSpringExecutor; @Resource private OllamaChatModel ollamaChatModel; @Test public void chatTest() { String model = "qwen3:0.6b"; String prompt = "背景说明:你是一个研发工程师,擅长解决技术类问题。"; String input = "请写一个java程序,实现一个方法,输入一个字符串,返回字符串的长度。"; // build chat-client ChatClient ollamaChatClient = ChatClient .builder(ollamaChatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()) .defaultAdvisors(SimpleLoggerAdvisor.builder().build()) .defaultOptions(OllamaChatOptions.builder().model(model).build()) .build(); // call ollama String response = ollamaChatClient .prompt(prompt) .user(input) .call() .content(); logger.info("input: {}", input); logger.info("response: {}", response); } @Test public void chatStreamTest() throws InterruptedException { String model = "qwen3:0.6b"; String prompt = "背景说明:你是一个研发工程师,擅长解决技术类问题。"; String input = "请写一个java程序,实现一个方法,输入一个字符串,返回字符串的长度。"; // build chat-client ChatClient ollamaChatClient = ChatClient .builder(ollamaChatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()) .defaultAdvisors(SimpleLoggerAdvisor.builder().build()) .defaultOptions(OllamaChatOptions.builder().model(model).build()) .build(); // call ollama logger.info("input: {}", input); Flux flux = ollamaChatClient .prompt(prompt) .user(input) .stream() .content(); flux.subscribe( data -> System.out.println("Received: " + data), // onNext 处理 error -> System.err.println("Error: " + error), // onError 处理 () -> System.out.println("Completed") // onComplete 处理 ); TimeUnit.SECONDS.sleep(10); } }