From baa6ff9cc98b473974d170069546bf6883e050d5 Mon Sep 17 00:00:00 2001 From: lzq Date: Tue, 29 Jul 2025 18:43:18 +0800 Subject: [PATCH] 1 --- .gitignore | 7 + .../njzscloud-common-cache/pom.xml | 40 + .../com/njzscloud/common/cache/Cache.java | 17 + .../com/njzscloud/common/cache/Caches.java | 30 + .../com/njzscloud/common/cache/DualCache.java | 75 + .../njzscloud/common/cache/FirstCache.java | 46 + .../com/njzscloud/common/cache/NoCache.java | 36 + .../njzscloud/common/cache/SecondCache.java | 103 + .../cache/config/CacheAutoConfiguration.java | 34 + .../common/cache/config/CacheProperties.java | 30 + .../main/resources/META-INF/spring.factories | 2 + .../njzscloud-common-core/pom.xml | 125 + .../common/core/ex/CliException.java | 25 + .../core/ex/ExceptionDepthComparator.java | 93 + .../common/core/ex/ExceptionMsg.java | 53 + .../common/core/ex/ExceptionType.java | 21 + .../njzscloud/common/core/ex/Exceptions.java | 77 + .../njzscloud/common/core/ex/ExpectData.java | 60 + .../njzscloud/common/core/ex/SysError.java | 26 + .../common/core/ex/SysException.java | 25 + .../common/core/ex/SysThrowable.java | 52 + .../common/core/fastjson/Fastjson.java | 139 + .../serializer/DictObjectDeserializer.java | 107 + .../serializer/DictObjectSerializer.java | 97 + .../common/core/http/HttpClient.java | 208 + .../http/HttpClientAutoConfiguration.java | 25 + .../common/core/http/HttpClientDecorator.java | 201 + .../core/http/HttpClientProperties.java | 62 + .../core/http/annotation/BodyParam.java | 23 + .../core/http/annotation/FormBodyParam.java | 32 + .../core/http/annotation/GetEndpoint.java | 28 + .../core/http/annotation/HeaderParam.java | 33 + .../core/http/annotation/JsonBodyParam.java | 21 + .../core/http/annotation/MultiBodyParam.java | 37 + .../core/http/annotation/PathParam.java | 34 + .../core/http/annotation/PostEndpoint.java | 28 + .../core/http/annotation/QueryParam.java | 32 + .../core/http/annotation/RemoteServer.java | 44 + .../core/http/annotation/XmlBodyParam.java | 21 + .../common/core/http/constant/HttpMethod.java | 8 + .../common/core/http/constant/Mime.java | 39 + .../interceptor/CompositeInterceptor.java | 47 + .../http/interceptor/RequestInterceptor.java | 11 + .../http/interceptor/ResponseInterceptor.java | 17 + .../http/processor/BodyParamProcessor.java | 7 + .../processor/DefaultBodyParamProcessor.java | 19 + .../DefaultFormBodyParamProcessor.java | 13 + .../DefaultHeaderParamProcessor.java | 13 + .../DefaultJsonBodyParamProcessor.java | 11 + .../DefaultMultiBodyParamProcessor.java | 13 + .../processor/DefaultPathParamProcessor.java | 13 + .../processor/DefaultQueryParamProcessor.java | 13 + .../DefaultXmlBodyParamProcessor.java | 11 + .../processor/FormBodyParamProcessor.java | 10 + .../http/processor/HeaderParamProcessor.java | 10 + .../processor/JsonBodyParamProcessor.java | 7 + .../processor/MultiBodyParamProcessor.java | 10 + .../http/processor/PathParamProcessor.java | 9 + .../http/processor/QueryParamProcessor.java | 10 + .../http/processor/XmlBodyParamProcessor.java | 7 + .../core/http/resolver/BodyParamResolver.java | 43 + .../http/resolver/FormBodyParamResolver.java | 97 + .../http/resolver/HeaderParamResolver.java | 92 + .../http/resolver/JsonBodyParamResolver.java | 41 + .../http/resolver/MultiBodyParamResolver.java | 141 + .../core/http/resolver/ParamResolver.java | 242 + .../core/http/resolver/PathParamResolver.java | 74 + .../http/resolver/QueryParamResolver.java | 92 + .../http/resolver/XmlBodyParamResolver.java | 41 + .../core/http/support/ParameterInfo.java | 10 + .../common/core/http/support/RequestInfo.java | 29 + .../core/http/support/ResponseInfo.java | 32 + .../core/http/support/ResponseResult.java | 97 + .../com/njzscloud/common/core/ienum/Dict.java | 70 + .../njzscloud/common/core/ienum/DictInt.java | 11 + .../njzscloud/common/core/ienum/DictStr.java | 10 + .../njzscloud/common/core/ienum/IEnum.java | 24 + .../common/core/jackson/Jackson.java | 229 + .../jackson/serializer/BigDecimalModule.java | 16 + .../serializer/BigDecimalSerializer.java | 22 + .../jackson/serializer/DictDeserializer.java | 102 + .../jackson/serializer/DictSerializer.java | 61 + .../core/jackson/serializer/LongModule.java | 15 + .../jackson/serializer/LongSerializer.java | 21 + .../core/jackson/serializer/TimeModule.java | 29 + .../com/njzscloud/common/core/thread/Q.java | 688 + .../common/core/thread/ThreadPool.java | 138 + .../core/thread/WindowBlockingQueue.java | 693 + .../com/njzscloud/common/core/tree/Tree.java | 137 + .../njzscloud/common/core/tree/TreeNode.java | 51 + .../njzscloud/common/core/tuple/Tuple2.java | 121 + .../njzscloud/common/core/tuple/Tuple3.java | 84 + .../njzscloud/common/core/tuple/Tuple4.java | 87 + .../njzscloud/common/core/utils/Globs.java | 195 + .../common/core/utils/GroupUtil.java | 625 + .../com/njzscloud/common/core/utils/Key.java | 41 + .../com/njzscloud/common/core/utils/R.java | 243 + .../njzscloud-common-email/pom.xml | 55 + .../njzscloud/common/email/MailMessage.java | 234 + .../common/email/util/EMailUtil.java | 98 + njzscloud-common/njzscloud-common-gen/pom.xml | 46 + .../common/gen/SysTplController.java | 94 + .../njzscloud/common/gen/SysTplEntity.java | 55 + .../njzscloud/common/gen/SysTplMapper.java | 12 + .../njzscloud/common/gen/SysTplService.java | 87 + .../gen/config/GenAutoConfiguration.java | 30 + .../common/gen/contant/TplCategory.java | 24 + .../com/njzscloud/common/gen/support/Btl.java | 40 + .../common/gen/support/DbMetaData.java | 73 + .../common/gen/support/Generator.java | 55 + .../common/gen/support/TemplateEngine.java | 75 + .../com/njzscloud/common/gen/support/Tpl.java | 26 + .../main/resources/META-INF/spring.factories | 2 + .../main/resources/templates/controller.btl | 74 + .../src/main/resources/templates/entity.btl | 56 + .../src/main/resources/templates/mapper.btl | 22 + .../main/resources/templates/mapper_xml.btl | 14 + .../src/main/resources/templates/service.btl | 69 + .../src/main/resources/templates/tpl.json | 10 + .../main/resources/templates/tpl_update.json | 8 + njzscloud-common/njzscloud-common-job/pom.xml | 56 + .../common/job/XxlAdminProperties.java | 26 + .../common/job/XxlExecutorProperties.java | 34 + .../common/job/XxlJobAutoConfiguration.java | 28 + .../common/job/XxlJobProperties.java | 15 + .../main/resources/META-INF/spring.factories | 2 + .../njzscloud-common-minio/pom.xml | 33 + .../minio/config/MinioAutoConfiguration.java | 22 + .../common/minio/config/MinioProperties.java | 18 + .../njzscloud/common/minio/util/Minio.java | 157 + .../main/resources/META-INF/spring.factories | 2 + njzscloud-common/njzscloud-common-mp/pom.xml | 71 + .../mp/config/DBTunnelAutoConfiguration.java | 19 + .../common/mp/config/DbTunnelProperties.java | 34 + .../common/mp/config/MpAutoConfiguration.java | 42 + .../mp/config/MybatisPlusProperties.java | 14 + .../support/CustomDataPermissionHandler.java | 43 + .../njzscloud/common/mp/support/DBTunnel.java | 114 + .../mp/support/MetaObjectHandlerImpl.java | 51 + .../common/mp/support/PageParam.java | 65 + .../common/mp/support/PageResult.java | 46 + .../handler/e/EnumTypeHandlerDealer.java | 123 + .../mp/support/handler/j/JsonTypeHandler.java | 48 + .../main/resources/META-INF/spring.factories | 3 + njzscloud-common/njzscloud-common-mvc/pom.xml | 79 + .../mvc/config/MvcAutoConfiguration.java | 47 + ...appingHandlerAdapterAutoConfiguration.java | 72 + .../DictHandlerMethodArgumentResolver.java | 60 + .../support/GlobalExceptionController.java | 311 + .../support/ReusableHttpServletRequest.java | 146 + .../mvc/support/ReusableRequestFilter.java | 17 + .../common/mvc/util/FileResponseUtil.java | 97 + .../common/mvc/validator/Constrained.java | 5 + .../common/mvc/validator/Constraint.java | 17 + .../mvc/validator/ConstraintValidator.java | 31 + .../common/mvc/validator/ValidRule.java | 22 + .../ws/config/WebsocketAutoConfiguration.java | 75 + .../mvc/ws/config/WebsocketProperties.java | 43 + .../ws/support/TokenHandshakeInterceptor.java | 43 + .../support/WebSocketChannelInterceptor.java | 47 + .../njzscloud/common/mvc/ws/util/WsUtil.java | 33 + .../main/resources/META-INF/spring.factories | 4 + .../njzscloud-common-redis/pom.xml | 58 + .../com/njzscloud/common/redis/RedisCli.java | 232 + .../njzscloud/common/redis/annotation/Eg.java | 38 + .../common/redis/annotation/RedisChannel.java | 33 + .../redis/annotation/RedisListener.java | 19 + .../redis/config/RedisOtherProperties.java | 22 + .../config/RedisServiceAutoConfiguration.java | 120 + .../redis/support/RedisFastjsonCodec.java | 63 + .../redis/support/RedisListenerRegistrar.java | 100 + .../redis/support/RedisMessageDispatch.java | 157 + .../njzscloud/common/redis/util/Redis.java | 759 + .../main/resources/META-INF/spring.factories | 2 + .../njzscloud-common-security/pom.xml | 54 + .../config/WebSecurityAutoConfiguration.java | 161 + .../config/WebSecurityProperties.java | 24 + .../common/security/contant/AuthWay.java | 23 + .../common/security/contant/Constants.java | 32 + .../security/contant/EndpointAccessModel.java | 22 + .../security/ex/ForbiddenAccessException.java | 19 + .../ex/MissingPermissionException.java | 16 + .../security/ex/UserLoginException.java | 29 + .../handler/AccessDeniedExceptionHandler.java | 52 + .../handler/AuthExceptionHandler.java | 46 + .../security/handler/LoginPostHandler.java | 97 + .../security/handler/LogoutPostHandler.java | 54 + .../PasswordAuthenticationProvider.java | 62 + .../module/password/PasswordLoginForm.java | 42 + .../password/PasswordLoginPreparer.java | 45 + .../permission/DefaultPermissionManager.java | 24 + .../permission/PermissionManager.java | 177 + .../PermissionSecurityMetaDataSource.java | 48 + .../security/permission/PermissionVoter.java | 43 + .../security/permission/RolePermission.java | 35 + .../AbstractAuthenticationProvider.java | 135 + .../support/AuthenticationDetails.java | 55 + .../CombineAuthenticationConfigurer.java | 83 + .../support/CombineAuthenticationFilter.java | 143 + .../DefaultAuthenticationProvider.java | 21 + .../support/DefaultLoginHistoryRecorder.java | 16 + .../security/support/EndpointResource.java | 47 + .../security/support/IResourceService.java | 6 + .../common/security/support/IRoleService.java | 8 + .../security/support/ITokenService.java | 12 + .../common/security/support/IUserService.java | 5 + .../common/security/support/LoginForm.java | 20 + .../common/security/support/LoginHistory.java | 66 + .../support/LoginHistoryRecorder.java | 9 + .../security/support/LoginPreparer.java | 24 + .../common/security/support/MenuResource.java | 69 + .../common/security/support/Resource.java | 17 + .../common/security/support/Token.java | 109 + .../TokenSecurityContextRepository.java | 125 + .../security/support/TokenSerializer.java | 21 + .../support/UserAuthenticationToken.java | 72 + .../common/security/support/UserDetail.java | 110 + .../common/security/util/EncryptUtil.java | 64 + .../common/security/util/SecurityUtil.java | 61 + .../main/resources/META-INF/spring.factories | 2 + .../njzscloud-common-sichen/pom.xml | 38 + .../common/sichen/SysTaskEntity.java | 65 + .../common/sichen/SysTaskMapper.java | 12 + .../common/sichen/SysTaskService.java | 73 + .../sichen/config/TaskAutoConfiguration.java | 50 + .../common/sichen/config/TaskProperties.java | 27 + .../common/sichen/contant/ScheduleType.java | 17 + .../common/sichen/contant/TaskStatus.java | 20 + .../sichen/dispatcher/SichenScheduler.java | 130 + .../sichen/executor/SichenExecutor.java | 43 + .../common/sichen/support/Cable.java | 46 + .../common/sichen/support/CronExpression.java | 1670 + .../njzscloud/common/sichen/support/Task.java | 19 + .../common/sichen/support/TaskHandle.java | 72 + .../common/sichen/support/TaskInfo.java | 48 + .../common/sichen/support/TaskStore.java | 106 + .../common/sichen/support/TaskUtil.java | 39 + .../main/resources/META-INF/spring.factories | 2 + njzscloud-common/njzscloud-common-sn/pom.xml | 42 + .../njzscloud/common/sn/AddSnConfigParam.java | 42 + .../common/sn/ModifySnConfigParam.java | 38 + .../njzscloud/common/sn/SysIncSnEntity.java | 61 + .../njzscloud/common/sn/SysIncSnMapper.java | 12 + .../njzscloud/common/sn/SysIncSnService.java | 77 + .../common/sn/SysSnConfigController.java | 86 + .../common/sn/SysSnConfigEntity.java | 47 + .../common/sn/SysSnConfigMapper.java | 12 + .../common/sn/SysSnConfigService.java | 120 + .../common/sn/config/SnAutoConfiguration.java | 30 + .../common/sn/config/SnProperties.java | 15 + .../njzscloud/common/sn/contant/PadMode.java | 21 + .../common/sn/contant/RandomMode.java | 21 + .../common/sn/contant/SnSection.java | 22 + .../common/sn/support/FixedSection.java | 13 + .../common/sn/support/FixedSectionConfig.java | 32 + .../common/sn/support/ISnSection.java | 5 + .../common/sn/support/IncSection.java | 30 + .../common/sn/support/IncSectionConfig.java | 50 + .../common/sn/support/RandomSection.java | 42 + .../sn/support/RandomSectionConfig.java | 53 + .../common/sn/support/SectionConfig.java | 15 + .../com/njzscloud/common/sn/support/Sn.java | 26 + .../njzscloud/common/sn/support/SnUtil.java | 47 + .../common/sn/support/TimeSection.java | 19 + .../common/sn/support/TimeSectionConfig.java | 45 + .../main/resources/META-INF/spring.factories | 2 + njzscloud-common/pom.xml | 32 + njzscloud-svr/pom.xml | 103 + .../java/com/njzscloud/supervisory/Main.java | 12 + .../auth/mapper/SysTokenMapper.java | 13 + .../supervisory/auth/pojo/SysTokenEntity.java | 50 + .../auth/service/SecurityService.java | 36 + .../auth/service/TokenService.java | 68 + .../dict/controller/SysDictController.java | 94 + .../controller/SysDictItemController.java | 81 + .../dict/mapper/SysDictItemMapper.java | 13 + .../dict/mapper/SysDictMapper.java | 13 + .../dict/pojo/ObtainDictDataResult.java | 60 + .../supervisory/dict/pojo/SysDictEntity.java | 44 + .../dict/pojo/SysDictItemEntity.java | 59 + .../dict/service/SysDictItemService.java | 77 + .../dict/service/SysDictService.java | 91 + .../controller/SysDistrictController.java | 87 + .../district/mapper/SysDistrictMapper.java | 32 + .../district/pojo/DistrictTreeResult.java | 64 + .../district/pojo/SysDistrictEntity.java | 65 + .../district/service/SysDistrictService.java | 84 + .../endpoint/contant/RequestMethod.java | 21 + .../controller/SysEndpointController.java | 81 + .../endpoint/mapper/SysEndpointMapper.java | 16 + .../endpoint/pojo/SysEndpointEntity.java | 85 + .../endpoint/service/SysEndpointService.java | 85 + .../menu/contant/MenuCategory.java | 21 + .../menu/controller/SysMenuController.java | 97 + .../menu/mapper/SysMenuMapper.java | 15 + .../supervisory/menu/pojo/MenuAddParam.java | 74 + .../menu/pojo/MenuDetailResult.java | 57 + .../menu/pojo/MenuModifyParam.java | 56 + .../menu/pojo/MenuSearchParam.java | 27 + .../supervisory/menu/pojo/SysMenuEntity.java | 100 + .../menu/service/SysMenuService.java | 196 + .../oss/controller/OSSController.java | 86 + .../cotroller/SysResourceController.java | 95 + .../resource/mapper/SysResourceMapper.java | 21 + .../resource/pojo/SysResourceEntity.java | 43 + .../resource/service/SysResourceService.java | 89 + .../role/controller/SysRoleController.java | 115 + .../role/mapper/SysRoleMapper.java | 31 + .../role/mapper/SysRoleResMapper.java | 13 + .../supervisory/role/pojo/RoleAddParam.java | 32 + .../role/pojo/RoleBindResourceParam.java | 24 + .../role/pojo/RoleDetailResult.java | 40 + .../role/pojo/RoleModifyParam.java | 34 + .../supervisory/role/pojo/RoleQueryParam.java | 21 + .../supervisory/role/pojo/SysRoleEntity.java | 70 + .../role/pojo/SysRoleResourceEntity.java | 49 + .../role/service/SysRoleResService.java | 77 + .../role/service/SysRoleService.java | 189 + .../supervisory/user/contant/Gender.java | 21 + .../user/controller/SysUserController.java | 106 + .../user/mapper/SysUserAccountMapper.java | 29 + .../user/mapper/SysUserMapper.java | 26 + .../user/mapper/SysUserRoleMapper.java | 18 + .../user/pojo/AddUserAccountParam.java | 68 + .../supervisory/user/pojo/AddUserParam.java | 48 + .../user/pojo/ModifyInfoParam.java | 23 + .../user/pojo/ModifyPasswdParam.java | 13 + .../supervisory/user/pojo/MyResult.java | 51 + .../user/pojo/ObtainInfoResult.java | 94 + .../user/pojo/SysUserAccountEntity.java | 108 + .../supervisory/user/pojo/SysUserEntity.java | 83 + .../user/pojo/SysUserRoleEntity.java | 35 + .../user/pojo/UserAccountDetailResult.java | 43 + .../user/pojo/UserAccountModifyParam.java | 68 + .../user/pojo/UserDetailResult.java | 49 + .../user/pojo/UserModifyParam.java | 45 + .../supervisory/user/pojo/UserQueryParam.java | 37 + .../user/pojo/UserRegisterParam.java | 201 + .../user/service/SysUserAccountService.java | 101 + .../user/service/SysUserRoleService.java | 76 + .../user/service/SysUserService.java | 236 + .../src/main/resources/application-dev.yml | 49 + .../src/main/resources/application-prod.yml | 31 + .../src/main/resources/application.yml | 74 + .../src/main/resources/logback-spring.xml | 37 + .../resources/mapper/SysEndpointMapper.xml | 26 + .../main/resources/mapper/SysMenuMapper.xml | 26 + .../resources/mapper/SysResourceMapper.xml | 49 + .../main/resources/mapper/SysRoleMapper.xml | 29 + .../resources/mapper/SysUserAccountMapper.xml | 46 + .../main/resources/mapper/SysUserMapper.xml | 37 + .../resources/mapper/SysUserRoleMapper.xml | 18 + pom.xml | 201 + xxl-job/pom.xml | 107 + xxl-job/xxl-job-admin/pom.xml | 94 + .../xxl/job/admin/XxlJobAdminApplication.java | 16 + .../job/admin/controller/IndexController.java | 96 + .../admin/controller/JobApiController.java | 72 + .../admin/controller/JobCodeController.java | 96 + .../admin/controller/JobGroupController.java | 204 + .../admin/controller/JobInfoController.java | 172 + .../admin/controller/JobLogController.java | 246 + .../job/admin/controller/UserController.java | 179 + .../annotation/PermissionLimit.java | 29 + .../interceptor/CookieInterceptor.java | 42 + .../interceptor/PermissionInterceptor.java | 59 + .../controller/interceptor/WebMvcConfig.java | 28 + .../resolver/WebExceptionResolver.java | 66 + .../xxl/job/admin/core/alarm/JobAlarm.java | 20 + .../xxl/job/admin/core/alarm/JobAlarmer.java | 65 + .../admin/core/alarm/impl/EmailJobAlarm.java | 118 + .../admin/core/complete/XxlJobCompleter.java | 99 + .../admin/core/conf/XxlJobAdminConfig.java | 158 + .../job/admin/core/cron/CronExpression.java | 1666 + .../admin/core/exception/XxlJobException.java | 14 + .../xxl/job/admin/core/model/XxlJobGroup.java | 77 + .../xxl/job/admin/core/model/XxlJobInfo.java | 237 + .../xxl/job/admin/core/model/XxlJobLog.java | 157 + .../job/admin/core/model/XxlJobLogGlue.java | 75 + .../job/admin/core/model/XxlJobLogReport.java | 54 + .../job/admin/core/model/XxlJobRegistry.java | 55 + .../xxl/job/admin/core/model/XxlJobUser.java | 73 + .../job/admin/core/old/RemoteHttpJobBean.java | 32 + .../core/old/XxlJobDynamicScheduler.java | 413 + .../job/admin/core/old/XxlJobThreadPool.java | 58 + .../core/route/ExecutorRouteStrategyEnum.java | 48 + .../job/admin/core/route/ExecutorRouter.java | 24 + .../route/strategy/ExecutorRouteBusyover.java | 48 + .../strategy/ExecutorRouteConsistentHash.java | 85 + .../route/strategy/ExecutorRouteFailover.java | 48 + .../route/strategy/ExecutorRouteFirst.java | 19 + .../core/route/strategy/ExecutorRouteLFU.java | 79 + .../core/route/strategy/ExecutorRouteLRU.java | 76 + .../route/strategy/ExecutorRouteLast.java | 19 + .../route/strategy/ExecutorRouteRandom.java | 23 + .../route/strategy/ExecutorRouteRound.java | 46 + .../core/scheduler/MisfireStrategyEnum.java | 39 + .../core/scheduler/ScheduleTypeEnum.java | 46 + .../admin/core/scheduler/XxlJobScheduler.java | 101 + .../admin/core/thread/JobCompleteHelper.java | 184 + .../core/thread/JobFailMonitorHelper.java | 110 + .../admin/core/thread/JobLogReportHelper.java | 152 + .../admin/core/thread/JobRegistryHelper.java | 204 + .../admin/core/thread/JobScheduleHelper.java | 369 + .../core/thread/JobTriggerPoolHelper.java | 150 + .../admin/core/trigger/TriggerTypeEnum.java | 27 + .../job/admin/core/trigger/XxlJobTrigger.java | 226 + .../xxl/job/admin/core/util/CookieUtil.java | 98 + .../com/xxl/job/admin/core/util/FtlUtil.java | 31 + .../com/xxl/job/admin/core/util/I18nUtil.java | 79 + .../xxl/job/admin/core/util/JacksonUtil.java | 92 + .../job/admin/core/util/LocalCacheUtil.java | 133 + .../com/xxl/job/admin/dao/XxlJobGroupDao.java | 37 + .../com/xxl/job/admin/dao/XxlJobInfoDao.java | 49 + .../com/xxl/job/admin/dao/XxlJobLogDao.java | 62 + .../xxl/job/admin/dao/XxlJobLogGlueDao.java | 24 + .../xxl/job/admin/dao/XxlJobLogReportDao.java | 26 + .../xxl/job/admin/dao/XxlJobRegistryDao.java | 38 + .../com/xxl/job/admin/dao/XxlJobUserDao.java | 31 + .../xxl/job/admin/service/LoginService.java | 107 + .../xxl/job/admin/service/XxlJobService.java | 98 + .../job/admin/service/impl/AdminBizImpl.java | 35 + .../admin/service/impl/XxlJobServiceImpl.java | 473 + .../src/main/resources/application.yml | 78 + .../main/resources/i18n/message_en.properties | 276 + .../resources/i18n/message_zh_CN.properties | 275 + .../resources/i18n/message_zh_TC.properties | 275 + .../src/main/resources/logback-spring.xml | 37 + .../mybatis-mapper/XxlJobGroupMapper.xml | 95 + .../mybatis-mapper/XxlJobInfoMapper.xml | 244 + .../mybatis-mapper/XxlJobLogGlueMapper.xml | 72 + .../mybatis-mapper/XxlJobLogMapper.xml | 275 + .../mybatis-mapper/XxlJobLogReportMapper.xml | 63 + .../mybatis-mapper/XxlJobRegistryMapper.xml | 63 + .../mybatis-mapper/XxlJobUserMapper.xml | 89 + .../Ionicons/css/ionicons.min.css | 11 + .../Ionicons/fonts/ionicons.eot | Bin 0 -> 120724 bytes .../Ionicons/fonts/ionicons.svg | 2231 + .../Ionicons/fonts/ionicons.ttf | Bin 0 -> 188508 bytes .../Ionicons/fonts/ionicons.woff | Bin 0 -> 67904 bytes .../bower_components/PACE/pace.min.js | 2 + .../PACE/themes/blue/pace-theme-flash.css | 111 + .../daterangepicker.css | 418 + .../daterangepicker.js | 1658 + .../bootstrap/css/bootstrap.min.css | 6 + .../bootstrap/css/bootstrap.min.css.map | 1 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 295 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes .../bootstrap/js/bootstrap.min.js | 6 + .../css/dataTables.bootstrap.min.css | 1 + .../js/dataTables.bootstrap.min.js | 8 + .../js/jquery.dataTables.min.js | 166 + .../bower_components/fastclick/fastclick.js | 844 + .../font-awesome/css/font-awesome.css.map | 7 + .../font-awesome/css/font-awesome.min.css | 4 + .../font-awesome/fonts/FontAwesome.otf | Bin 0 -> 134808 bytes .../fonts/fontawesome-webfont.eot | Bin 0 -> 165742 bytes .../fonts/fontawesome-webfont.svg | 2672 + .../fonts/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../fonts/fontawesome-webfont.woff | Bin 0 -> 98024 bytes .../fonts/fontawesome-webfont.woff2 | Bin 0 -> 77160 bytes .../jquery.slimscroll.min.js | 16 + .../bower_components/jquery/jquery.min.js | 2 + .../bower_components/moment/moment.min.js | 1 + .../static/adminlte/dist/css/AdminLTE.min.css | 8 + .../dist/css/skins/_all-skins.min.css | 1 + .../static/adminlte/dist/js/adminlte.min.js | 13 + .../adminlte/plugins/iCheck/icheck.min.js | 10 + .../adminlte/plugins/iCheck/square/blue.css | 67 + .../adminlte/plugins/iCheck/square/blue.png | Bin 0 -> 2185 bytes .../plugins/iCheck/square/blue@2x.png | Bin 0 -> 4485 bytes .../src/main/resources/static/favicon.ico | Bin 0 -> 4286 bytes .../src/main/resources/static/js/common.1.js | 156 + .../src/main/resources/static/js/index.js | 207 + .../resources/static/js/jobcode.index.1.js | 98 + .../resources/static/js/jobgroup.index.1.js | 370 + .../resources/static/js/jobinfo.index.1.js | 739 + .../resources/static/js/joblog.detail.1.js | 91 + .../resources/static/js/joblog.index.1.js | 396 + .../src/main/resources/static/js/login.1.js | 66 + .../main/resources/static/js/user.index.1.js | 328 + .../codemirror/addon/hint/anyword-hint.js | 41 + .../codemirror/addon/hint/show-hint.css | 36 + .../codemirror/addon/hint/show-hint.js | 486 + .../plugins/codemirror/lib/codemirror.css | 534 + .../plugins/codemirror/lib/codemirror.js | 12373 + .../plugins/codemirror/mode/clike/clike.js | 898 + .../codemirror/mode/javascript/javascript.js | 1062 + .../static/plugins/codemirror/mode/php/php.js | 246 + .../codemirror/mode/powershell/powershell.js | 397 + .../plugins/codemirror/mode/python/python.js | 423 + .../plugins/codemirror/mode/shell/shell.js | 155 + .../static/plugins/cronGen/cronGen.js | 1107 + .../static/plugins/cronGen/cronGen_en.js | 1107 + .../plugins/echarts/echarts.common.min.js | 22 + .../static/plugins/jquery/jquery.cookie.js | 118 + .../plugins/jquery/jquery.validate.min.js | 4 + .../resources/static/plugins/layer/layer.js | 2 + .../plugins/layer/theme/default/icon-ext.png | Bin 0 -> 5911 bytes .../plugins/layer/theme/default/icon.png | Bin 0 -> 11493 bytes .../plugins/layer/theme/default/layer.css | 1 + .../plugins/layer/theme/default/loading-0.gif | Bin 0 -> 5793 bytes .../plugins/layer/theme/default/loading-1.gif | Bin 0 -> 701 bytes .../plugins/layer/theme/default/loading-2.gif | Bin 0 -> 1787 bytes .../templates/common/common.exception.ftl | 42 + .../templates/common/common.macro.ftl | 239 + .../src/main/resources/templates/help.ftl | 47 + .../src/main/resources/templates/index.ftl | 147 + .../templates/jobcode/jobcode.index.ftl | 164 + .../templates/jobgroup/jobgroup.index.ftl | 191 + .../templates/jobinfo/jobinfo.index.ftl | 540 + .../templates/joblog/joblog.detail.ftl | 70 + .../templates/joblog/joblog.index.ftl | 180 + .../src/main/resources/templates/login.ftl | 45 + .../resources/templates/user/user.index.ftl | 188 + xxl-job/xxl-job-core/pom.xml | 64 + .../java/com/xxl/job/core/biz/AdminBiz.java | 48 + .../com/xxl/job/core/biz/ExecutorBiz.java | 45 + .../job/core/biz/client/AdminBizClient.java | 50 + .../core/biz/client/ExecutorBizClient.java | 56 + .../job/core/biz/impl/ExecutorBizImpl.java | 172 + .../core/biz/model/HandleCallbackParam.java | 67 + .../xxl/job/core/biz/model/IdleBeatParam.java | 28 + .../com/xxl/job/core/biz/model/KillParam.java | 28 + .../com/xxl/job/core/biz/model/LogParam.java | 47 + .../com/xxl/job/core/biz/model/LogResult.java | 56 + .../xxl/job/core/biz/model/RegistryParam.java | 54 + .../com/xxl/job/core/biz/model/ReturnT.java | 57 + .../xxl/job/core/biz/model/TriggerParam.java | 144 + .../xxl/job/core/context/XxlJobContext.java | 122 + .../xxl/job/core/context/XxlJobHelper.java | 255 + .../core/enums/ExecutorBlockStrategyEnum.java | 35 + .../xxl/job/core/enums/RegistryConfig.java | 13 + .../xxl/job/core/executor/XxlJobExecutor.java | 271 + .../executor/impl/XxlJobSimpleExecutor.java | 75 + .../executor/impl/XxlJobSpringExecutor.java | 147 + .../com/xxl/job/core/glue/GlueFactory.java | 90 + .../com/xxl/job/core/glue/GlueTypeEnum.java | 53 + .../job/core/glue/impl/SpringGlueFactory.java | 80 + .../com/xxl/job/core/handler/IJobHandler.java | 38 + .../core/handler/annotation/JobHandler.java | 24 + .../job/core/handler/annotation/XxlJob.java | 30 + .../job/core/handler/impl/GlueJobHandler.java | 38 + .../core/handler/impl/MethodJobHandler.java | 53 + .../core/handler/impl/ScriptJobHandler.java | 93 + .../xxl/job/core/log/XxlJobFileAppender.java | 220 + .../com/xxl/job/core/server/EmbedServer.java | 256 + .../core/thread/ExecutorRegistryThread.java | 129 + .../core/thread/JobLogFileCleanThread.java | 124 + .../com/xxl/job/core/thread/JobThread.java | 252 + .../core/thread/TriggerCallbackThread.java | 260 + .../java/com/xxl/job/core/util/DateUtil.java | 156 + .../java/com/xxl/job/core/util/FileUtil.java | 181 + .../java/com/xxl/job/core/util/GsonTool.java | 88 + .../java/com/xxl/job/core/util/IpUtil.java | 203 + .../xxl/job/core/util/JdkSerializeTool.java | 73 + .../java/com/xxl/job/core/util/NetUtil.java | 70 + .../com/xxl/job/core/util/ScriptUtil.java | 228 + .../com/xxl/job/core/util/ShardingUtil.java | 46 + .../com/xxl/job/core/util/ThrowableUtil.java | 24 + .../xxl/job/core/util/XxlJobRemotingUtil.java | 159 + z-doc/a.js | 66 + z-doc/area.json | 22997 + z-doc/city.json | 2024 + z-doc/convertToSql.js | 175 + z-doc/district_data.sql | 45045 ++ z-doc/http/SysMenu.http | 73 + z-doc/http/test.http | 11 + z-doc/http/公司信息.http | 125 + z-doc/http/其他.http | 142 + z-doc/http/登录注册.http | 54 + z-doc/http/项目.http | 146 + z-doc/id.js | 62 + z-doc/init-sql/JSJ-DDL.sql | 654 + z-doc/init-sql/JSJ-DML.sql | 78 + z-doc/init-sql/xxl_job.sql | 157 + z-doc/init-sql/市.sql | 0 z-doc/init-sql/省.sql | 35 + z-doc/province.json | 172 + z-doc/supervisory.pdma.json | 16298 + z-doc/town.json | 330226 +++++++++++++++ z-doc/xxl-job.png | Bin 0 -> 239306 bytes z-doc/xxl-job.svg | 776 + 586 files changed, 489763 insertions(+) create mode 100644 .gitignore create mode 100644 njzscloud-common/njzscloud-common-cache/pom.xml create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java create mode 100644 njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-core/pom.xml create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClient.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientDecorator.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientProperties.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/BodyParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/FormBodyParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/GetEndpoint.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/HeaderParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/JsonBodyParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/MultiBodyParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PathParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PostEndpoint.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/QueryParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/RemoteServer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/XmlBodyParam.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/HttpMethod.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/Mime.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/CompositeInterceptor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/RequestInterceptor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/ResponseInterceptor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/BodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultFormBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultHeaderParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultJsonBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultMultiBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultPathParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultQueryParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultXmlBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/FormBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/HeaderParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/JsonBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/MultiBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/PathParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/QueryParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/XmlBodyParamProcessor.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/BodyParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/FormBodyParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/HeaderParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/JsonBodyParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/MultiBodyParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/ParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/PathParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/QueryParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/XmlBodyParamResolver.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ParameterInfo.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/RequestInfo.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseInfo.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseResult.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java create mode 100644 njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java create mode 100644 njzscloud-common/njzscloud-common-email/pom.xml create mode 100644 njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java create mode 100644 njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java create mode 100644 njzscloud-common/njzscloud-common-gen/pom.xml create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplController.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplEntity.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplMapper.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplService.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/controller.btl create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/entity.btl create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper.btl create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper_xml.btl create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/service.btl create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json create mode 100644 njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl_update.json create mode 100644 njzscloud-common/njzscloud-common-job/pom.xml create mode 100644 njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlAdminProperties.java create mode 100644 njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlExecutorProperties.java create mode 100644 njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobProperties.java create mode 100644 njzscloud-common/njzscloud-common-job/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-minio/pom.xml create mode 100644 njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioProperties.java create mode 100644 njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/util/Minio.java create mode 100644 njzscloud-common/njzscloud-common-minio/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-mp/pom.xml create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DBTunnelAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DbTunnelProperties.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/DBTunnel.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java create mode 100644 njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-mvc/pom.xml create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/FileResponseUtil.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketProperties.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/TokenHandshakeInterceptor.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/WebSocketChannelInterceptor.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/util/WsUtil.java create mode 100644 njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-redis/pom.xml create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java create mode 100644 njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-security/pom.xml create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/EndpointResource.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IResourceService.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IRoleService.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IUserService.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/MenuResource.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Resource.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java create mode 100644 njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-sichen/pom.xml create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskEntity.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskMapper.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskService.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/dispatcher/SichenScheduler.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Cable.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskUtil.java create mode 100644 njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/njzscloud-common-sn/pom.xml create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnEntity.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnMapper.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnService.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigController.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigEntity.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigMapper.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigService.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java create mode 100644 njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring.factories create mode 100644 njzscloud-common/pom.xml create mode 100644 njzscloud-svr/pom.xml create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/mapper/SysTokenMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/pojo/SysTokenEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/SecurityService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/TokenService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictItemController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictItemMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/ObtainDictDataResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictItemEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictItemService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/controller/SysDistrictController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/mapper/SysDistrictMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/DistrictTreeResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/SysDistrictEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/service/SysDistrictService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/contant/RequestMethod.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/controller/SysEndpointController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/mapper/SysEndpointMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/pojo/SysEndpointEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/service/SysEndpointService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/contant/MenuCategory.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/controller/SysMenuController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/mapper/SysMenuMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuAddParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuDetailResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuModifyParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuSearchParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/SysMenuEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/service/SysMenuService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/oss/controller/OSSController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/cotroller/SysResourceController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/mapper/SysResourceMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/pojo/SysResourceEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/service/SysResourceService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/controller/SysRoleController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleResMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleAddParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleBindResourceParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleDetailResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleModifyParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleQueryParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleResourceEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleResService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/contant/Gender.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/controller/SysUserController.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserAccountMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserRoleMapper.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserAccountParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyInfoParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyPasswdParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/MyResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ObtainInfoResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserAccountEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserRoleEntity.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountDetailResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountModifyParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserDetailResult.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserModifyParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserQueryParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserRegisterParam.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserAccountService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserRoleService.java create mode 100644 njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserService.java create mode 100644 njzscloud-svr/src/main/resources/application-dev.yml create mode 100644 njzscloud-svr/src/main/resources/application-prod.yml create mode 100644 njzscloud-svr/src/main/resources/application.yml create mode 100644 njzscloud-svr/src/main/resources/logback-spring.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysEndpointMapper.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysMenuMapper.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysResourceMapper.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysRoleMapper.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysUserAccountMapper.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysUserMapper.xml create mode 100644 njzscloud-svr/src/main/resources/mapper/SysUserRoleMapper.xml create mode 100644 pom.xml create mode 100644 xxl-job/pom.xml create mode 100644 xxl-job/xxl-job-admin/pom.xml create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java create mode 100644 xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java create mode 100644 xxl-job/xxl-job-admin/src/main/resources/application.yml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties create mode 100644 xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties create mode 100644 xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties create mode 100644 xxl-job/xxl-job-admin/src/main/resources/logback-spring.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff2 create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/js/bootstrap.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/datatables.net/js/jquery.dataTables.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/fastclick/fastclick.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/css/font-awesome.css.map create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/css/font-awesome.min.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/fonts/FontAwesome.otf create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/fonts/fontawesome-webfont.eot create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/fonts/fontawesome-webfont.svg create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/fonts/fontawesome-webfont.ttf create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/fonts/fontawesome-webfont.woff create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/font-awesome/fonts/fontawesome-webfont.woff2 create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/jquery-slimscroll/jquery.slimscroll.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/jquery/jquery.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/moment/moment.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/dist/css/AdminLTE.min.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/dist/css/skins/_all-skins.min.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/dist/js/adminlte.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/index.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif create mode 100644 xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl create mode 100644 xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl create mode 100644 xxl-job/xxl-job-core/pom.xml create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java create mode 100644 xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java create mode 100644 z-doc/a.js create mode 100644 z-doc/area.json create mode 100644 z-doc/city.json create mode 100644 z-doc/convertToSql.js create mode 100644 z-doc/district_data.sql create mode 100644 z-doc/http/SysMenu.http create mode 100644 z-doc/http/test.http create mode 100644 z-doc/http/公司信息.http create mode 100644 z-doc/http/其他.http create mode 100644 z-doc/http/登录注册.http create mode 100644 z-doc/http/项目.http create mode 100644 z-doc/id.js create mode 100644 z-doc/init-sql/JSJ-DDL.sql create mode 100644 z-doc/init-sql/JSJ-DML.sql create mode 100644 z-doc/init-sql/xxl_job.sql create mode 100644 z-doc/init-sql/市.sql create mode 100644 z-doc/init-sql/省.sql create mode 100644 z-doc/province.json create mode 100644 z-doc/supervisory.pdma.json create mode 100644 z-doc/town.json create mode 100644 z-doc/xxl-job.png create mode 100644 z-doc/xxl-job.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b894a45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/**/logs +/**/*.iml +/**/.idea +/**/target +/**/.DS_Store +/**/.xcodemap +/**/.back* diff --git a/njzscloud-common/njzscloud-common-cache/pom.xml b/njzscloud-common/njzscloud-common-cache/pom.xml new file mode 100644 index 0000000..3bdd6ce --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-cache + jar + + njzscloud-common-cache + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + com.njzscloud + njzscloud-common-redis + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java new file mode 100644 index 0000000..555c690 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.cache; + +import java.util.function.Supplier; + +public interface Cache { + T get(String key); + + T get(String key, Supplier supplier); + + T get(String key, long timeout, Supplier supplier); + + void put(String key, Object value, long timeout); + + void put(String key, Object value); + + void remove(String key); +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java new file mode 100644 index 0000000..57fc7ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.cache; + +import cn.hutool.extra.spring.SpringUtil; + +import java.util.function.Supplier; + +public final class Caches { + + public static final Cache CACHE = SpringUtil.getBean(Cache.class); + + public static T get(String key) { + return CACHE.get(key); + } + + public static T get(String key, long timeout, Supplier supplier) { + return CACHE.get(key, timeout, supplier); + } + + public static void put(String key, Object value, long timeout) { + CACHE.put(key, value, timeout); + } + + public static void put(String key, Object value) { + CACHE.put(key, value); + } + + public static void remove(String key) { + CACHE.remove(key); + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java new file mode 100644 index 0000000..89e56e6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java @@ -0,0 +1,75 @@ +package com.njzscloud.common.cache; + +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +public class DualCache implements Cache { + private final FirstCache FIRST_CACHE; + private final SecondCache SECOND_CACHE; + private final ReentrantReadWriteLock DUAL_CACHE_LOCK = new ReentrantReadWriteLock(); + + public DualCache(int capacity, long firstTimeout, long secondTimeout) { + FIRST_CACHE = new FirstCache(capacity, firstTimeout); + SECOND_CACHE = new SecondCache(secondTimeout); + } + + public T get(String key) { + DUAL_CACHE_LOCK.readLock().lock(); + try { + return FIRST_CACHE.get(key, () -> SECOND_CACHE.get(key)); + } finally { + DUAL_CACHE_LOCK.readLock().unlock(); + } + } + + public T get(String key, Supplier supplier) { + DUAL_CACHE_LOCK.readLock().lock(); + try { + return (T) FIRST_CACHE.get(key, () -> SECOND_CACHE.get(key, supplier)); + } finally { + DUAL_CACHE_LOCK.readLock().unlock(); + } + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + DUAL_CACHE_LOCK.readLock().lock(); + try { + return (T) FIRST_CACHE.get(key, timeout, () -> SECOND_CACHE.get(key, timeout, supplier)); + } finally { + DUAL_CACHE_LOCK.readLock().unlock(); + } + } + + public void put(String key, Object value) { + DUAL_CACHE_LOCK.writeLock().lock(); + try { + FIRST_CACHE.put(key, value); + SECOND_CACHE.put(key, value); + } finally { + DUAL_CACHE_LOCK.writeLock().unlock(); + } + } + + public void put(String key, Object value, long timeout) { + DUAL_CACHE_LOCK.writeLock().lock(); + try { + FIRST_CACHE.put(key, value); + SECOND_CACHE.put(key, value, timeout); + } finally { + DUAL_CACHE_LOCK.writeLock().unlock(); + } + } + + public void remove(String key) { + DUAL_CACHE_LOCK.writeLock().lock(); + try { + FIRST_CACHE.remove(key); + SECOND_CACHE.remove(key); + } finally { + DUAL_CACHE_LOCK.writeLock().unlock(); + } + } +} + + diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java new file mode 100644 index 0000000..68c095e --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.cache; + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.LFUCache; + +import java.util.function.Supplier; + +@SuppressWarnings("unchecked") +public class FirstCache implements Cache { + private final LFUCache CACHE; + + public FirstCache(int capacity, long timeout) { + CACHE = CacheUtil.newLFUCache(capacity, timeout); + CacheUtil.newNoCache().get("first", null); + } + + @Override + public T get(String key) { + return (T) CACHE.get(key); + } + + public T get(String key, Supplier supplier) { + return (T) CACHE.get(key, supplier::get); + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + return (T) CACHE.get(key, true, timeout, supplier::get); + } + + @Override + public void put(String key, Object value, long timeout) { + CACHE.put(key, value, timeout); + } + + @Override + public void put(String key, Object value) { + CACHE.put(key, value); + } + + @Override + public void remove(String key) { + CACHE.remove(key); + } + +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java new file mode 100644 index 0000000..81927f4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java @@ -0,0 +1,36 @@ +package com.njzscloud.common.cache; + +import java.util.function.Supplier; + +public class NoCache implements Cache { + + public NoCache() { + } + + @Override + public T get(String key) { + return null; + } + + public T get(String key, Supplier supplier) { + return supplier.get(); + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + return supplier.get(); + } + + @Override + public void put(String key, Object value, long timeout) { + } + + @Override + public void put(String key, Object value) { + } + + @Override + public void remove(String key) { + } + +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java new file mode 100644 index 0000000..d08a540 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java @@ -0,0 +1,103 @@ +package com.njzscloud.common.cache; + +import com.njzscloud.common.redis.util.Redis; +import io.lettuce.core.SetArgs; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +@SuppressWarnings("unchecked") +public class SecondCache implements Cache { + + private final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>(); + private final long timeout; + + public SecondCache(long timeout) { + this.timeout = timeout; + } + + + public T get(String key) { + ReentrantReadWriteLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); + lock.readLock().lock(); + try { + return Redis.get(key); + } finally { + LOCKS.remove(key); + lock.readLock().unlock(); + } + } + + @Override + public T get(String key, Supplier supplier) { + Object o = Redis.get(key); + if (o != null) return (T) o; + + ReentrantReadWriteLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); + lock.writeLock().lock(); + try { + o = Redis.get(key); + if (o != null) return (T) o; + o = supplier.get(); + if (o != null) Redis.set(key, o); + return (T) o; + } finally { + LOCKS.remove(key); + lock.writeLock().unlock(); + } + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + Object o = Redis.get(key); + if (o != null) return (T) o; + + ReentrantReadWriteLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); + lock.writeLock().lock(); + try { + o = Redis.get(key); + if (o != null) return (T) o; + o = supplier.get(); + if (o != null) Redis.set(key, o, new SetArgs().ex(timeout)); + return (T) o; + } finally { + LOCKS.remove(key); + lock.writeLock().unlock(); + } + } + + public void put(String key, Object value, long timeout) { + ReentrantReadWriteLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); + lock.writeLock().lock(); + try { + Redis.set(key, value, new SetArgs().ex(timeout)); + } finally { + LOCKS.remove(key); + lock.writeLock().unlock(); + } + } + + public void put(String key, Object value) { + ReentrantReadWriteLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); + lock.writeLock().lock(); + try { + if (timeout > 0) Redis.set(key, value, new SetArgs().ex(timeout)); + else Redis.set(key, value); + } finally { + LOCKS.remove(key); + lock.writeLock().unlock(); + } + } + + public void remove(String key) { + ReentrantReadWriteLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); + lock.writeLock().lock(); + try { + Redis.del(key); + } finally { + LOCKS.remove(key); + lock.writeLock().unlock(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java new file mode 100644 index 0000000..48edecf --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java @@ -0,0 +1,34 @@ +package com.njzscloud.common.cache.config; + +import com.njzscloud.common.cache.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(CacheProperties.class) +public class CacheAutoConfiguration { + + @Bean + public Cache cache(CacheProperties properties) { + CacheProperties.FirstCacheProperties first = properties.getFirst(); + CacheProperties.SecondCacheProperties second = properties.getSecond(); + Cache cache; + if (first.isEnabled()) { + if (second.isEnabled()) { + cache = new DualCache(first.getCapacity(), first.getTimeout(), second.getTimeout()); + } else { + cache = new FirstCache(first.getCapacity(), first.getTimeout()); + } + } else { + if (second.isEnabled()) { + cache = new SecondCache(second.getTimeout()); + } else { + cache = new NoCache(); + } + } + return cache; + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java new file mode 100644 index 0000000..15d6114 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.cache.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "cache") +public class CacheProperties { + + private FirstCacheProperties first = new FirstCacheProperties(); + + private SecondCacheProperties second = new SecondCacheProperties(); + + @Getter + @Setter + public static class FirstCacheProperties { + private boolean enabled = true; + private int capacity = 100000; + private long timeout = 3600 * 24; + } + + @Getter + @Setter + public static class SecondCacheProperties { + private boolean enabled = false; + private long timeout = 3600 * 24; + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..2d68baf --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.cache.config.CacheAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-core/pom.xml b/njzscloud-common/njzscloud-common-core/pom.xml new file mode 100644 index 0000000..37e4aef --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-core + + + 8 + 8 + UTF-8 + + + + + + com.alibaba + easyexcel + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + cn.hutool + hutool-cache + + + cn.hutool + hutool-core + + + cn.hutool + hutool-extra + + + cn.hutool + hutool-captcha + + + cn.hutool + hutool-crypto + + + + + + cglib + cglib + + + + + com.squareup.okhttp3 + okhttp + + + + + org.projectlombok + lombok + + + + + org.slf4j + slf4j-api + + + + + ch.qos.logback + logback-classic + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java new file mode 100644 index 0000000..2a8746e --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 客户端异常, 表示客户端参错误 + */ +public class CliException extends SysThrowable { + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected CliException(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(cause, expect, msg, message); + } + + @Override + public String getMessage() { + return StrUtil.format("客户端错误, 错误码: {}, 错误信息: {}, 详细信息: {}", this.msg.code, this.msg.msg, this.message); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java new file mode 100644 index 0000000..a16a861 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java @@ -0,0 +1,93 @@ +package com.njzscloud.common.core.ex; + + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.SimpleCache; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + *

用于两个异常之间的排序

+ *

根据当前异常与目标异常在异常继承体系中的层级进行比较

+ * + * @see org.springframework.core.ExceptionDepthComparator + */ +public class ExceptionDepthComparator implements Comparator> { + + /** + * 比较器缓存 + */ + private static final SimpleCache, ExceptionDepthComparator> CACHE = new SimpleCache<>(); + + /** + * 目标异常类型 + */ + private final Class targetException; + + /** + * 创建异常比较器 + * + * @param exception 目标异常 + */ + public ExceptionDepthComparator(Throwable exception) { + Assert.notNull(exception, "目标异常不能为空"); + this.targetException = exception.getClass(); + } + + /** + * 创建异常比较器 + * + * @param exceptionType 目标异常类型 + */ + public ExceptionDepthComparator(Class exceptionType) { + Assert.notNull(exceptionType, "目标异常类型不能为空"); + this.targetException = exceptionType; + } + + /** + * 从给定异常中获取最接近目标异常的匹配项 + * + * @param exceptionTypes 待匹配的异常列表 + * @param targetException 目标异常 + * @return 匹配到的异常 + */ + public static Class findClosestMatch(Collection> exceptionTypes, Throwable targetException) { + Assert.notEmpty(exceptionTypes, "不能为空"); + Assert.notNull(targetException, "不能为空"); + if (exceptionTypes.size() == 1) { + return exceptionTypes.iterator().next(); + } + List> handledExceptions = new ArrayList<>(exceptionTypes); + ExceptionDepthComparator comparator = CACHE.get(targetException.getClass(), () -> new ExceptionDepthComparator(targetException)); + handledExceptions.sort(comparator); + return handledExceptions.get(0); + } + + @Override + public int compare(Class o1, Class o2) { + int depth1 = getDepth(o1, this.targetException); + int depth2 = getDepth(o2, this.targetException); + return depth1 - depth2; + } + + /** + * 获取异常的层级深度 + * + * @param declaredException 待匹配的异常 + * @param exceptionToMatch 目标异常 + * @return 深度(≥0),越近数字越小 + */ + private int getDepth(Class declaredException, Class exceptionToMatch) { + int depth = 0; + do { + if (exceptionToMatch.equals(declaredException)) return depth; + if (exceptionToMatch == Throwable.class) return Integer.MAX_VALUE; + depth++; + exceptionToMatch = exceptionToMatch.getSuperclass(); + } while (true); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java new file mode 100644 index 0000000..c9cbebc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java @@ -0,0 +1,53 @@ +package com.njzscloud.common.core.ex; + + +/** + * 异常信息 + *

默认:

+ *

系统异常:SYS_EXP_MSG

+ *

客户端错误:CLI_ERR_MSG

+ *

系统错误:SYS_ERR_MSG

+ */ +public final class ExceptionMsg { + + /** + * 通用系统异常 + */ + public static final ExceptionMsg SYS_EXP_MSG = new ExceptionMsg(ExceptionType.SYS_EXP, 1111, "操作失败"); + + /** + * 通用客户端错误 + */ + public static final ExceptionMsg CLI_ERR_MSG = new ExceptionMsg(ExceptionType.CLI_ERR, 5555, "操作失败"); + + /** + * 通用系统错误 + */ + public static final ExceptionMsg SYS_ERR_MSG = new ExceptionMsg(ExceptionType.SYS_ERR, 9999, "系统错误"); + + /** + * 编号(0< code ≤9999) + */ + public final int code; + /** + * 异常信息(应尽量简略) + */ + public final String msg; + /** + * 异常类型 {@link ExceptionType} + */ + public final ExceptionType type; + + /** + * 创建异常信息 + * + * @param type 异常类型 {@link ExceptionType} + * @param code 异常编号(0< code ≤9999) + * @param msg 异常信息(应尽量简略) + */ + private ExceptionMsg(ExceptionType type, int code, String msg) { + this.code = code; + this.type = type; + this.msg = msg; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java new file mode 100644 index 0000000..3615dfb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.core.ex; + +import lombok.RequiredArgsConstructor; + +/** + * 异常类型 + */ +@RequiredArgsConstructor +public enum ExceptionType { + + SYS_EXP(1, "系统异常"), + + CLI_ERR(5, "客户端错误"), + + SYS_ERR(9, "系统错误"); + + public final int code; + + public final String name; + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java new file mode 100644 index 0000000..3643126 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java @@ -0,0 +1,77 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 异常工具类 + * 创建异常对象 + */ +public class Exceptions { + + public static SysThrowable clierr(Object message, Object... param) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.CLI_ERR_MSG, message, param); + } + + public static SysThrowable clierr(Object message) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.CLI_ERR_MSG, message, null); + } + + public static SysThrowable exception(Object message, Object... param) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_EXP_MSG, message, param); + } + + public static SysThrowable exception(Object message) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_EXP_MSG, message, null); + } + + public static SysThrowable error(Object message, Object... param) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, param); + } + + public static SysThrowable error(Object message) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, null); + } + + public static SysThrowable error(Throwable cause, Object message, Object... param) { + return build(cause, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, param); + } + + public static SysThrowable error(Throwable cause, Object message) { + return build(cause, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, null); + } + + /** + *

创建异常对象

+ * + * @param cause 嵌套异常 + * @param expect 期望值 + * @param msg 异常信息 + * @param message 详细信息(字符串模板, 占位符: {}) + * @param param 模板参数 + * @see SysThrowable + * @see ExceptionMsg + * @see ExpectData + */ + private static SysThrowable build(Throwable cause, ExpectData expect, ExceptionMsg msg, Object message, Object[] param) { + + if (message instanceof String + && !StrUtil.isBlank((String) message) + && param != null + && param.length > 0) { + message = StrUtil.format((String) message, param); + } + + SysThrowable sysThrowable; + + if (msg.type == ExceptionType.SYS_EXP) { + sysThrowable = new SysException(cause, expect, msg, message); + } else if (msg.type == ExceptionType.CLI_ERR) { + sysThrowable = new CliException(cause, expect, msg, message); + } else { + sysThrowable = new SysError(cause, expect, msg, message); + } + + return sysThrowable; + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java new file mode 100644 index 0000000..b1f8be2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java @@ -0,0 +1,60 @@ +package com.njzscloud.common.core.ex; + +import lombok.AllArgsConstructor; + +import java.util.Collections; + +/** + * 期望值 + *

HTTP 响应时 响应体的 JSON 内容

+ */ +@AllArgsConstructor +public enum ExpectData { + + /** + * 响应: null + */ + NULL_DATA(null), + + /** + * 响应: [] + */ + ARR_DATA(Collections.emptyList()), + + /** + * 响应: {} + */ + KV_DATA(Collections.emptyMap()), + + /** + * 响应: "" + */ + STR_BLANK_DATA(""), + + /** + * 响应: 0 + */ + NUM_ZERO_DATA(0), + + /** + * 响应: false + */ + BOOL_FALSE_DATA(Boolean.FALSE), + + /** + * 响应: true + */ + BOOL_TRUE_DATA(Boolean.TRUE), + + /** + * 无响应体 + */ + VOID_DATA(Void.TYPE); + + private final Object data; + + @SuppressWarnings("unchecked") + public T getData() { + return (T) data; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java new file mode 100644 index 0000000..ca6b3df --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 系统错误, 此类异常无法处理或不应该处理 + */ +public class SysError extends SysThrowable { + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected SysError(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(cause, expect, msg, message); + } + + + @Override + public String getMessage() { + return StrUtil.format("内部服务错误, 错误码: {}, 错误信息: {}, 详细信息: {}", this.msg.code, this.msg.msg, this.message); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java new file mode 100644 index 0000000..64f1e98 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 系统异常, 表示可预料的错误或可预料的情况 + */ +public class SysException extends SysThrowable { + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected SysException(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(cause, expect, msg, message); + } + + @Override + public String getMessage() { + return StrUtil.format("系统异常, 错误码: {}, 错误信息: {}, 详细信息: {}", this.msg.code, this.msg.msg, this.message); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java new file mode 100644 index 0000000..1e9eb7b --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java @@ -0,0 +1,52 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.jackson.Jackson; + +/** + * 系统异常 + */ +public abstract class SysThrowable extends RuntimeException { + + /** + * 希望值 + * + * @see ExpectData + */ + public final Object expect; + + /** + * 异常信息(简略) + */ + public final ExceptionMsg msg; + + /** + * 异常信息(详细) + */ + public final Object message; + + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected SysThrowable(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(msg.msg, cause); + this.msg = msg; + this.message = message == null ? "" : message; + this.expect = expect; + } + + @Override + public String toString() { + return Jackson.toJsonStr(MapUtil.builder() + .put("code", msg.code) + .put("expect", expect) + .put("msg", msg.msg) + .put("message", message) + .build()); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java new file mode 100644 index 0000000..757d4eb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java @@ -0,0 +1,139 @@ +package com.njzscloud.common.core.fastjson; + +import cn.hutool.core.date.DatePattern; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.alibaba.fastjson2.filter.ValueFilter; +import com.alibaba.fastjson2.reader.ObjectReader; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStream; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import static com.alibaba.fastjson2.JSONReader.Feature.IgnoreCheckClose; + +/** + *

Fastjson 工具

+ *

已开启类型序列化/反序列化支持

+ */ +@Slf4j +public class Fastjson { + private static final JSONWriter.Context JSON_WRITER_CONTEXT; + private static final JSONReader.Context JSON_READER_CONTEXT; + + static { + JSON_WRITER_CONTEXT = new JSONWriter.Context( + JSONWriter.Feature.WriteNulls, // 序列化输出空值字段 + JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数,输出为字符串格式 + JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息 + JSONWriter.Feature.PrettyFormat, // 格式化输出 + JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature + JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString,避免科学计数法 + JSONWriter.Feature.WriteNullListAsEmpty, // 将List类型字段的空值序列化输出为空数组"[]" + JSONWriter.Feature.WriteNullStringAsEmpty, // 将String类型字段的空值序列化输出为空字符串"" + JSONWriter.Feature.WriteLongAsString // 将 Long 作为字符串输出 + ); + + ZoneId zoneId = ZoneId.of("GMT+8"); + + JSON_WRITER_CONTEXT.setZoneId(zoneId); + JSON_WRITER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN); + + JSON_WRITER_CONTEXT.configFilter((ValueFilter) (object, name, value) -> { + if (value != null) { + Class clazz = value.getClass(); + if (clazz == LocalDate.class) { + return DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN).withZone(zoneId).format((LocalDate) value); + } else if (clazz == LocalTime.class) { + return DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN).withZone(zoneId).format((LocalTime) value); + } else if (clazz == BigDecimal.class) { + return ((BigDecimal) value).toPlainString(); + } + } + return value; + }); + JSON_READER_CONTEXT = new JSONReader.Context( + JSONReader.Feature.IgnoreSetNullValue, // 忽略输入为null的字段 + JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据,需要显示打开SupportAutoType + ); + JSON_READER_CONTEXT.setZoneId(zoneId); + JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN); + } + + /** + * 序列化为 JSON 字符串 + * + * @param object 数据 + * @return String + * @see JSON#toJSONString(Object, JSONWriter.Context) + */ + public static String toJsonStr(Object object) { + return JSON.toJSONString(object, JSON_WRITER_CONTEXT); + } + + /** + * 序列化为 JSON 字节数组 + * + * @param object 数据 + * @return byte[] + * @see JSON#toJSONBytes(Object, Filter...) + */ + public static byte[] toJsonBytes(Object object) { + return JSON.toJSONBytes(object, StandardCharsets.UTF_8, JSON_WRITER_CONTEXT); + } + + /** + * 反序列化 + * + * @param json JSON 字符串 + * @param type 类型 + * @return T + * @see JSON#parseObject(String, Class, JSONReader.Context) + */ + @SuppressWarnings("unchecked") + public static T toBean(String json, Type type) { + if (json == null || json.isEmpty()) { + return null; + } + return toBean(JSONReader.of(json, JSON_READER_CONTEXT), type); + } + + public static T toBean(InputStream json, Type type) { + if (json == null) { + return null; + } + return toBean(JSONReader.of(json, StandardCharsets.UTF_8, JSON_READER_CONTEXT), type); + + } + + public static T toBean(byte[] json, Type type) { + if (json == null || json.length == 0) { + return null; + } + return toBean(JSONReader.of(json, JSON_READER_CONTEXT), type); + } + + @SuppressWarnings("unchecked") + private static T toBean(JSONReader reader, Type type) { + ObjectReader objectReader = JSON_READER_CONTEXT.getObjectReader(type); + try { + T object = objectReader.readObject(reader, type, null, 0); + reader.handleResolveTasks(object); + if (!reader.isEnd() && (JSON_READER_CONTEXT.getFeatures() & IgnoreCheckClose.mask) == 0) { + throw new JSONException(reader.info("input not end")); + } + return object; + } finally { + reader.close(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java new file mode 100644 index 0000000..548f665 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java @@ -0,0 +1,107 @@ +package com.njzscloud.common.core.fastjson.serializer; + + +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import com.alibaba.fastjson2.util.TypeUtils; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Dict 枚举的 Fastjson 反序列化器
+ * 使用方式(二选一即可):
+ * 1、在字段上使用 JSONField 进行指定
+ * 如,@JSONField(deserializeUsing = DictObjectSerializer.class)
+ * 2、在枚举类上使用 JSONType 进行指定(在接口上指定无效)
+ * 如,@JSONType(writeEnumAsJavaBean = true, deserializer = DictObjectSerializer.class)

+ * JSON 格式见对应的序列化器 {@link DictObjectSerializer} + * + * @see Dict + * @see DictInt + * @see DictStr + * @see DictObjectSerializer + */ +@Slf4j +public class DictObjectDeserializer implements ObjectReader { + + private static final ClassLoader CLASSLOADER = DictObjectDeserializer.class.getClassLoader(); + + @Override + public Dict readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) { + try { + if (jsonReader.isObject()) { + Map map = jsonReader.readObject(); + + String typeField = (String) map.get(IEnum.ENUM_TYPE); + Object valField = map.get(Dict.ENUM_VAL); + + + Class clazz = CLASSLOADER.loadClass(typeField); + + if (valField instanceof String) { + if (DictStr.class.isAssignableFrom(clazz)) { + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse((String) valField, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(Integer.parseInt((String) valField), constants); + } else { + return null; + } + } else if (valField instanceof Integer) { + if (DictStr.class.isAssignableFrom(clazz)) { + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(String.valueOf(valField), constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse((Integer) valField, constants); + } else { + return null; + } + } else { + return null; + } + } else { + if (jsonReader.isString()) { + Class clazz = TypeUtils.getClass(fieldType); + if (DictStr.class.isAssignableFrom(clazz)) { + String val = jsonReader.readString(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + String val = jsonReader.readString(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(Integer.parseInt(val), constants); + } else { + return null; + } + } else if (jsonReader.isInt()) { + Class clazz = TypeUtils.getClass(fieldType); + if (DictStr.class.isAssignableFrom(clazz)) { + String val = jsonReader.readString(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + Integer val = jsonReader.readInt32(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else { + return null; + } + } else { + return null; + } + } + } catch (Exception e) { + log.error("字典枚举反序列化失败", e); + throw Exceptions.error(e, "字典枚举反序列化失败,类型:{},字段名:{}", fieldType, fieldName); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java new file mode 100644 index 0000000..38f1ce2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.core.fastjson.serializer; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.util.TypeUtils; +import com.alibaba.fastjson2.writer.ObjectWriter; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; + +import java.lang.reflect.Type; + +/** + * Dict 枚举的 Fastjson 序列化器
+ * 使用方式(二选一即可):
+ * 1、在字段上使用 JSONField 进行指定
+ * 如,@JSONField(serializeUsing = DictObjectSerializer.class)
+ * 2、在枚举类上使用 JSONType 进行指定(在接口上指定无效)
+ * 如,@JSONType(writeEnumAsJavaBean = true, serializer = DictObjectSerializer.class)

+ * JSON 格式
+ * 1、枚举不是其他对象的属性
+ *
+ * {
+ *   "type": "", // 枚举全限定类名, 反序列化时会用到
+ *   "name": "", // name 属性
+ *   "ordinal": 0, // ordinal 属性
+ *   "val": 1,  // val 属性(字符串/数字), 反序列化时会用到
+ *   "txt": "1" // txt 属性
+ * }
+ * 2、枚举是其他对象的属性
+ *
+ * {
+ *   // ... 其他属性
+ *   "原字段名称": 1, // val 属性(字符串/数字), 反序列化时会用到
+ *   "原字段名称Txt": "1" //  txt 属性
+ * }
+ * + * @see Dict + * @see DictInt + * @see DictStr + */ +public class DictObjectSerializer implements ObjectWriter { + + @Override + public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) { + if (object == null) { + jsonWriter.writeNull(); + return; + } + + if (fieldType == null) { + Dict dict = (Dict) object; + jsonWriter.startObject(); + + jsonWriter.writeName(IEnum.ENUM_TYPE); + jsonWriter.writeColon(); + jsonWriter.writeString(dict.getClass().getName()); + + jsonWriter.writeName(IEnum.ENUM_NAME); + jsonWriter.writeColon(); + jsonWriter.writeString(((Enum) dict).name()); + + jsonWriter.writeName(IEnum.ENUM_ORDINAL); + jsonWriter.writeColon(); + jsonWriter.writeInt32(((Enum) dict).ordinal()); + + jsonWriter.writeName(Dict.ENUM_VAL); + jsonWriter.writeColon(); + jsonWriter.writeAny(dict.getVal()); + + jsonWriter.writeName(Dict.ENUM_TXT); + jsonWriter.writeColon(); + jsonWriter.writeString(dict.getTxt()); + + jsonWriter.endObject(); + } else { + Class clazz = TypeUtils.getClass(fieldType); + + if (DictInt.class.isAssignableFrom(clazz)) { + DictInt dictInt = (DictInt) object; + jsonWriter.writeInt32(dictInt.getVal()); + + jsonWriter.writeName(fieldName + "Txt"); + jsonWriter.writeColon(); + jsonWriter.writeString(dictInt.getTxt()); + } else if (DictStr.class.isAssignableFrom(clazz)) { + DictStr dictStr = (DictStr) object; + jsonWriter.writeString(dictStr.getVal()); + jsonWriter.writeName(fieldName + "Txt"); + jsonWriter.writeColon(); + jsonWriter.writeString(dictStr.getTxt()); + } else { + jsonWriter.writeNull(); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClient.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClient.java new file mode 100644 index 0000000..518ceed --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClient.java @@ -0,0 +1,208 @@ +package com.njzscloud.common.core.http; + +import cn.hutool.core.collection.CollUtil; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.support.ResponseInfo; +import com.njzscloud.common.core.tuple.Tuple2; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * HTTP 客户端 + * + * @see okhttp3.OkHttpClient + */ +@Slf4j +public final class HttpClient { + private static volatile HttpClient DEFAULT; + + private final OkHttpClient OK_HTTP_CLIENT; + + public HttpClient(HttpClientProperties httpClientProperties) { + this(httpClientProperties, null); + } + + public HttpClient(OkHttpClient okHttpClient) { + OK_HTTP_CLIENT = okHttpClient; + } + + public HttpClient(HttpClientProperties httpClientProperties, ExecutorService executorService) { + Duration callTimeout = httpClientProperties.getCallTimeout(); + Duration keepAliveTime = httpClientProperties.getKeepAliveTime(); + long keepAliveTimeSeconds = keepAliveTime.getSeconds(); + int maxIdleConnections = httpClientProperties.getMaxIdleConnections(); + Duration readTimeout = httpClientProperties.getReadTimeout(); + Duration connectTimeout = httpClientProperties.getConnectTimeout(); + + ConnectionPool connectionPool = new ConnectionPool(maxIdleConnections, keepAliveTimeSeconds, TimeUnit.SECONDS); + + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .callTimeout(callTimeout) + .readTimeout(readTimeout) + .connectTimeout(connectTimeout) + .connectionPool(connectionPool); + if (executorService != null) { + Dispatcher dispatcher = new Dispatcher(executorService); + builder.dispatcher(dispatcher); + } + + this.OK_HTTP_CLIENT = builder.build(); + } + + private HttpClient() { + this(HttpClientProperties.DEFAULT); + } + + public static synchronized HttpClient defaultHttpClient() { + if (DEFAULT == null) DEFAULT = new HttpClient(); + return DEFAULT; + } + + /* private Request buildRequest(HttpServer httpServer, + HttpEndpoint httpEndpoint, + RequestParamBuilder requestParamBuilder, + RequestInterceptor requestInterceptor) { + + + if (requestInterceptor != null) { + RequestParam processed = requestInterceptor.process(httpServer, httpEndpoint, requestParamBuilder); + if (processed != null) requestParam = processed; + } + + HttpMethod httpMethod = httpEndpoint.httpMethod; + String url = httpEndpoint.urlTpl; + BodyParam bodyParam = requestParam.bodyParam; + + + Map pathParamMap = requestParam.pathParam.getParam(); + if (CollUtil.isNotEmpty(pathParamMap)) { + url = StrUtil.format(url, pathParamMap); + } + + Map queryParamMap = requestParam.queryParam.getParam(); + String query = Param.kvStr(queryParamMap); + if (StrUtil.isNotBlank(query)) { + query = "?" + query; + } + url = url + query; + + Headers headers = null; + Map headerParamMap = requestParam.headerParam.getParam(); + if (CollUtil.isNotEmpty(headerParamMap)) { + Headers.Builder headerBuilder = new Headers.Builder(); + Set> entries = headerParamMap.entrySet(); + for (Map.Entry entry : entries) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value != null) headerBuilder.set(key, value.toString()); + } + headers = headerBuilder.build(); + } + + + RequestBody postBody = null; + if (httpMethod != HttpMethod.GET) { + byte[] param = bodyParam.getParam(); + postBody = RequestBody.create(param, MediaType.parse(bodyParam.contentType)); + } + + Request.Builder requestBuilder = new Request.Builder() + .url(httpServer + url) + .method(httpMethod.name(), postBody); + + if (headers != null) { + requestBuilder.headers(headers); + } + + return requestBuilder.build(); + } + + public T execute( + HttpServer httpServer, + HttpEndpoint endpoint, + RequestParamBuilder requestParamBuilder, + Type responseType, + RequestInterceptor requestInterceptor, + ResponseInterceptor responseInterceptor + ) { + ResponseResult.ResponseResultBuilder responseResultBuilder = ResponseResult.builder(); + + Response response = null; + ResponseBody responseBody = null; + Integer code = null; + String message = null; + Headers headers = null; + byte[] body = new byte[0]; + + try { + Request request = buildRequest(httpServer, endpoint, requestParamBuilder, requestInterceptor); + Call call = this.OK_HTTP_CLIENT.newCall(request); + response = call.execute(); + responseBody = response.body(); + if (responseBody != null) { + body = responseBody.bytes(); + } + code = response.code(); + message = response.message(); + headers = response.headers(); + + responseResultBuilder. + code(code) + .status(message) + .headers(headers.toMultimap()) + .body(body) + .build(); + } catch (Exception e) { + log.error("", e); + responseResultBuilder.e(e); + } finally { + if (responseBody != null) responseBody.close(); + if (response != null) response.close(); + } + + return responseInterceptor.process( + responseType, + httpServer, endpoint, requestParam, + responseResultBuilder.build()); + } */ + + + public ResponseInfo execute(HttpMethod httpMethod, String url, Map headers, Tuple2 body) { + Request.Builder requestBuilder = new Request.Builder().url(url); + + if (httpMethod != HttpMethod.GET) { + requestBuilder.method(httpMethod.name(), + RequestBody.create(body.get_1(), + MediaType.parse(body.get_0()) + )); + } + + if (CollUtil.isNotEmpty(headers)) { + Headers.Builder headerBuilder = new Headers.Builder(); + headers.forEach(headerBuilder::set); + requestBuilder.headers(headerBuilder.build()); + } + + try ( + Response response = this.OK_HTTP_CLIENT + .newCall(requestBuilder.build()) + .execute(); + ResponseBody responseBody = response.body() + ) { + int code = response.code(); + String message = response.message(); + Map> responseHeaders = response.headers().toMultimap(); + byte[] bytes = responseBody == null ? new byte[0] : responseBody.bytes(); + return ResponseInfo.create(code, message, responseHeaders, bytes); + } catch (Exception e) { + log.error("", e); + return ResponseInfo.create(0, "http 请求失败", null, null); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientAutoConfiguration.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientAutoConfiguration.java new file mode 100644 index 0000000..1534dcd --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientAutoConfiguration.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.core.http; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + */ +@Configuration +@EnableConfigurationProperties(HttpClientProperties.class) +public class HttpClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public HttpClient httpClient(HttpClientProperties httpClientProperties) { + return new HttpClient(httpClientProperties); + } + + @Bean + public HttpClientDecorator httpClientDecorator(HttpClient httpClient) { + return new HttpClientDecorator(httpClient); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientDecorator.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientDecorator.java new file mode 100644 index 0000000..b8fd926 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientDecorator.java @@ -0,0 +1,201 @@ +package com.njzscloud.common.core.http; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.SimpleCache; +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.http.annotation.GetEndpoint; +import com.njzscloud.common.core.http.annotation.PostEndpoint; +import com.njzscloud.common.core.http.annotation.RemoteServer; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.interceptor.RequestInterceptor; +import com.njzscloud.common.core.http.interceptor.ResponseInterceptor; +import com.njzscloud.common.core.http.resolver.*; +import com.njzscloud.common.core.http.support.RequestInfo; +import com.njzscloud.common.core.http.support.ResponseInfo; +import com.njzscloud.common.core.http.support.ResponseResult; +import com.njzscloud.common.core.tuple.Tuple2; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 服务装饰器, 用于 "接口注解式" HTTP 请求配置, 使用 cglib 进行接口装饰 + *

注: 在使用异步请求时, 接口方法的返回值不能是 void, 除非 HTTP 请求确实没有返回值

+ */ +@SuppressWarnings("unchecked") +public class HttpClientDecorator { + private static final SimpleCache, Object> ENHANCER_CACHE = new SimpleCache<>(); + + private final HttpClient HTTP_CLIENT; + + public HttpClientDecorator(HttpClient httpClient) { + HTTP_CLIENT = httpClient; + } + + public T decorate(Class clazz) { + return (T) ENHANCER_CACHE.get(clazz, () -> { + MethodInterceptor methodInterceptor = new MethodInterceptorImpl(clazz, HTTP_CLIENT); + Enhancer enhancer = new Enhancer(); + enhancer.setInterfaces(new Class[]{clazz}); + enhancer.setCallback(methodInterceptor); + return enhancer.create(); + }); + } + + @SuppressWarnings("RedundantLengthCheck") + private static class MethodInterceptorImpl implements MethodInterceptor { + private static final Pattern ADDR_PATTERN = Pattern.compile("(?http(?:s)?):\\/\\/(?[0-9a-zA-Z_\\-\\.]+)(?::(?[0-9]+))?(?\\/\\S*)*"); + private final HttpClient HTTP_CLIENT; + private final String baseUrl; + private final RequestInterceptor requestInterceptor; + private final ResponseInterceptor responseInterceptor; + + public MethodInterceptorImpl(Class clazz, HttpClient httpClient) { + RemoteServer anno = clazz.getAnnotation(RemoteServer.class); + baseUrl = anno.value(); + Matcher matcher = ADDR_PATTERN.matcher(baseUrl); + Assert.isTrue(matcher.matches(), "地址不合法"); + Class requestedInterceptorClazz = anno.requestInterceptor(); + Class responseInterceptorClazz = anno.responseInterceptor(); + requestInterceptor = ReflectUtil.newInstance(requestedInterceptorClazz); + responseInterceptor = ReflectUtil.newInstance(responseInterceptorClazz); + + HTTP_CLIENT = httpClient; + } + + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + HttpMethod httpMethod; + String urlTpl; + String desc; + + GetEndpoint getEndpoint = method.getAnnotation(GetEndpoint.class); + if (getEndpoint == null) { + PostEndpoint postEndpoint = method.getAnnotation(PostEndpoint.class); + if (postEndpoint == null) { + // + return null; + } else { + httpMethod = HttpMethod.POST; + urlTpl = baseUrl + postEndpoint.value(); + desc = postEndpoint.desc(); + } + } else { + httpMethod = HttpMethod.GET; + urlTpl = baseUrl + getEndpoint.value(); + desc = getEndpoint.desc(); + } + + Object[] additional = requestInterceptor.process(httpMethod, urlTpl, args); + + Parameter[] parameters = method.getParameters(); + + PathParamResolver pathParamResolver = new PathParamResolver(); + QueryParamResolver queryParamResolver = new QueryParamResolver(); + HeaderParamResolver headerParamResolver = new HeaderParamResolver(); + BodyParamResolver bodyParamResolver = null; + JsonBodyParamResolver jsonBodyParamResolver = null; + XmlBodyParamResolver xmlBodyParamResolver = null; + FormBodyParamResolver formBodyParamResolver = null; + MultiBodyParamResolver multiBodyParamResolver = null; + + List> paramResolvers = ListUtil.list(false, + pathParamResolver, + queryParamResolver, + headerParamResolver + ); + + if (httpMethod != HttpMethod.GET) { + jsonBodyParamResolver = new JsonBodyParamResolver(); + xmlBodyParamResolver = new XmlBodyParamResolver(); + formBodyParamResolver = new FormBodyParamResolver(); + multiBodyParamResolver = new MultiBodyParamResolver(); + bodyParamResolver = new BodyParamResolver(); + paramResolvers.add(jsonBodyParamResolver); + paramResolvers.add(xmlBodyParamResolver); + paramResolvers.add(formBodyParamResolver); + paramResolvers.add(multiBodyParamResolver); + paramResolvers.add(bodyParamResolver); + } + + if (additional != null) { + for (Object o : additional) { + for (ParamResolver resolver : paramResolvers) { + resolver.resolve(o); + } + } + } + + for (int i = 0; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + for (ParamResolver resolver : paramResolvers) { + resolver.resolve(parameter, args[i]); + } + } + + urlTpl = pathParamResolver.resolve(httpMethod, urlTpl); + urlTpl = queryParamResolver.resolve(httpMethod, urlTpl); + Map headers = headerParamResolver.resolve(httpMethod, urlTpl); + Tuple2 body = null; + if (httpMethod != HttpMethod.GET) { + body = jsonBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = xmlBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = formBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = multiBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = bodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = Tuple2.create("", new byte[0]); + } + + ResponseInfo responseInfo = HTTP_CLIENT.execute(httpMethod, urlTpl, headers, body); + + RequestInfo requestInfo = RequestInfo.create(desc, httpMethod, urlTpl, headers, body); + + Type genericReturnType = method.getGenericReturnType(); + + Type returnType; + Type rawType; + + if (genericReturnType instanceof ParameterizedType) { + ParameterizedType typeWrap = (ParameterizedType) genericReturnType; + + rawType = typeWrap.getRawType(); + + if (rawType == ResponseResult.class) { + returnType = typeWrap.getActualTypeArguments()[0]; + } else { + returnType = typeWrap; + } + } else { + returnType = genericReturnType; + rawType = genericReturnType; + } + + Object res = responseInterceptor.process(requestInfo, responseInfo, returnType); + + if (ResponseResult.class == rawType) { + return ResponseResult + .builder() + .code(responseInfo.code) + .status(responseInfo.message) + .headers(responseInfo.header) + .body(res) + .e(responseInfo.e) + .build(); + } + + return res; + } + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientProperties.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientProperties.java new file mode 100644 index 0000000..8c8924d --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/HttpClientProperties.java @@ -0,0 +1,62 @@ +package com.njzscloud.common.core.http; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * HTTP 客户端配置 + */ +@Getter +@Setter +@Accessors(chain = true) +@ConfigurationProperties("okhttp-client") +public class HttpClientProperties { + + public static final HttpClientProperties DEFAULT = new HttpClientProperties(); + + /** + * 调用的超时 + * 覆盖解析 DNS、连接、写入请求正文、服务器处理和读取响应正文等。如果调用需要重定向或重试,所有都必须在一个超时期限内完成。 + * 默认 0,0 表示不限制 + */ + private Duration callTimeout = Duration.ofSeconds(0); + + /** + * 读取超时 + * 默认 10s,0 表示不限制 + */ + private Duration readTimeout = Duration.ofSeconds(10); + + /** + * 连接超时 + * TCP 套接字连接到目标主机超时时间 + * 默认 10s,0 表示不限制 + */ + private Duration connectTimeout = Duration.ofSeconds(10); + + /** + * 最大活跃连接数, IP 和 port 相同的 HTTP 请求通常共享同一个连接 + * 默认 500 + */ + private int maxIdleConnections = 500; + + /** + * 连接空闲时间 + * 默认 10min + */ + private Duration keepAliveTime = Duration.ofMinutes(10); + + /** + * 是否禁用 SSL 验证, 默认 false 不禁用 + */ + private boolean disableSslValidation = false; + + /** + * 是否允许重定向, 默认 true 允许 + */ + private boolean followRedirects = true; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/BodyParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/BodyParam.java new file mode 100644 index 0000000..85c93b8 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/BodyParam.java @@ -0,0 +1,23 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.BodyParamProcessor; +import com.njzscloud.common.core.http.processor.DefaultBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface BodyParam { + String contentType() default ""; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/FormBodyParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/FormBodyParam.java new file mode 100644 index 0000000..da1a901 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/FormBodyParam.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.FormBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormBodyParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default FormBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/GetEndpoint.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/GetEndpoint.java new file mode 100644 index 0000000..1772cd1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/GetEndpoint.java @@ -0,0 +1,28 @@ +package com.njzscloud.common.core.http.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; + +/** + * GET 请求端点注解 + */ +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GetEndpoint { + /** + * 端点地址 + * + * @return String + */ + String value() default ""; + + /** + * 端点描述 + * + * @return String + */ + String desc() default ""; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/HeaderParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/HeaderParam.java new file mode 100644 index 0000000..41990f1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/HeaderParam.java @@ -0,0 +1,33 @@ +package com.njzscloud.common.core.http.annotation; + + +import com.njzscloud.common.core.http.processor.DefaultHeaderParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HeaderParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultHeaderParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/JsonBodyParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/JsonBodyParam.java new file mode 100644 index 0000000..de470f2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/JsonBodyParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.DefaultJsonBodyParamProcessor; +import com.njzscloud.common.core.http.processor.JsonBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonBodyParam { + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultJsonBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/MultiBodyParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/MultiBodyParam.java new file mode 100644 index 0000000..85b70b1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/MultiBodyParam.java @@ -0,0 +1,37 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.MultiBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MultiBodyParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + String filename() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + String contentType() default ""; + + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default MultiBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PathParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PathParam.java new file mode 100644 index 0000000..47cfd04 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PathParam.java @@ -0,0 +1,34 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.DefaultPathParamProcessor; +import com.njzscloud.common.core.http.processor.PathParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PathParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultPathParamProcessor.class; + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PostEndpoint.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PostEndpoint.java new file mode 100644 index 0000000..394460f --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/PostEndpoint.java @@ -0,0 +1,28 @@ +package com.njzscloud.common.core.http.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; + +/** + * POST 请求端点注解 + */ +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostEndpoint { + /** + * 端点地址 + * + * @return String + */ + String value() default ""; + + /** + * 端点描述 + * + * @return String + */ + String desc() default ""; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/QueryParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/QueryParam.java new file mode 100644 index 0000000..43181f7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/QueryParam.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.DefaultQueryParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface QueryParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultQueryParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/RemoteServer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/RemoteServer.java new file mode 100644 index 0000000..965a90c --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/RemoteServer.java @@ -0,0 +1,44 @@ +package com.njzscloud.common.core.http.annotation; + + +import com.njzscloud.common.core.http.interceptor.CompositeInterceptor; +import com.njzscloud.common.core.http.interceptor.RequestInterceptor; +import com.njzscloud.common.core.http.interceptor.ResponseInterceptor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +/** + * HTTP 服务器配置 + */ +@Target({TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RemoteServer { + /** + * 服务器地址, 如, http://localhost:80/api + * + * @return String + */ + String value(); + + /** + * 请求拦截器, 在请求之前触发 + * + * @return RequestInterceptor.class + * @see RequestInterceptor + * @see CompositeInterceptor + */ + Class requestInterceptor() default CompositeInterceptor.class; + + /** + * 响应拦截器, 在请求之后触发 + * + * @return ResponseInterceptor.class + * @see ResponseInterceptor + * @see CompositeInterceptor + */ + Class responseInterceptor() default CompositeInterceptor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/XmlBodyParam.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/XmlBodyParam.java new file mode 100644 index 0000000..c0fb0e6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/annotation/XmlBodyParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.core.http.annotation; + +import com.njzscloud.common.core.http.processor.DefaultXmlBodyParamProcessor; +import com.njzscloud.common.core.http.processor.XmlBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface XmlBodyParam { + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultXmlBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/HttpMethod.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/HttpMethod.java new file mode 100644 index 0000000..232d6dc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/HttpMethod.java @@ -0,0 +1,8 @@ +package com.njzscloud.common.core.http.constant; + +/** + * HTTP 方法 + */ +public enum HttpMethod { + GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, CONNECT, PATCH +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/Mime.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/Mime.java new file mode 100644 index 0000000..fa1ae79 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/constant/Mime.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.core.http.constant; + +import cn.hutool.core.util.StrUtil; + +/** + * MIME + */ +public interface Mime { + String FORM = "application/x-www-form-urlencoded"; + String MULTIPART_FORM = "multipart/form-data"; + String BINARY = "application/octet-stream"; + String JSON = "application/json"; + String X_NDJSON = "application/x-ndjson"; + String XML = "application/xml"; + String TXT = "text/plain"; + String HTML = "text/html"; + String CSS = "text/css"; + String GIF = "image/gif"; + String JPG = "image/jpeg"; + String PNG = "image/png"; + String SVG = "image/svg+xml"; + String WEBP = "image/webp"; + String PDF = "image/pdf"; + String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + String PPT = "application/vnd.ms-powerpoint"; + String XLS = "application/vnd.ms-excel"; + String DOC = "application/msword"; + String ZIP = "application/zip"; + String MP3 = "audio/mpeg"; + String MP4 = "video/mp4"; + + + static String u8Val(String mime) { + return StrUtil.format("{};charset={}", mime, "UTF-8"); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/CompositeInterceptor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/CompositeInterceptor.java new file mode 100644 index 0000000..4963c6d --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/CompositeInterceptor.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.core.http.interceptor; + +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.support.RequestInfo; +import com.njzscloud.common.core.http.support.ResponseInfo; +import com.njzscloud.common.core.jackson.Jackson; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Type; + + +/** + * 组合式拦截器, 响应拦截器默认解析 JSON 类型参数 + * + * @see RequestInterceptor + * @see ResponseInterceptor + */ +@Slf4j +public class CompositeInterceptor implements RequestInterceptor, ResponseInterceptor { + + @Override + public Object[] process(HttpMethod method, String url, Object[] args) { + return null; + } + + @Override + public Object process(RequestInfo requestInfo, ResponseInfo responseInfo, Type responseType) { + System.out.println(Jackson.toJsonStr(requestInfo)); + System.out.println(new String(requestInfo.body)); + + Object data = new String(responseInfo.body); + + /* if (responseInfo.success) { + if (responseInfo.body != null) { + data = Jackson.toBean(responseInfo.body, responseType); + log.info("Jackson: {}", JSON.toJSONString(data)); + data = JSON.parseObject(responseInfo.body, responseType); + log.info("Fastjson: {}", JSON.toJSONString(data)); + } + } else { + log.error("HTTP请求失败"); + } */ + + + return data; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/RequestInterceptor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/RequestInterceptor.java new file mode 100644 index 0000000..f718c3a --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/RequestInterceptor.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.core.http.interceptor; + + +import com.njzscloud.common.core.http.constant.HttpMethod; + +/** + * 请求拦截器, 在请求之前触发, 可修改请求参数 + */ +public interface RequestInterceptor { + Object[] process(HttpMethod method, String url, Object[] args); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/ResponseInterceptor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/ResponseInterceptor.java new file mode 100644 index 0000000..7048514 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/interceptor/ResponseInterceptor.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.core.http.interceptor; + + +import com.njzscloud.common.core.http.support.RequestInfo; +import com.njzscloud.common.core.http.support.ResponseInfo; + +import java.lang.reflect.Type; + +/** + * 响应拦截器, 在请求之后触发, + * 无论请求是否成功或是发生异常都会调用, + * 可用于响应结果解析 + */ +public interface ResponseInterceptor { + + Object process(RequestInfo requestInfo, ResponseInfo responseInfo, Type responseType); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/BodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/BodyParamProcessor.java new file mode 100644 index 0000000..9d71fb6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/BodyParamProcessor.java @@ -0,0 +1,7 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.BodyParam; + +public interface BodyParamProcessor { + byte[] process(BodyParam bodyParam, String paramName, Class paramClazz, Object paramValue); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultBodyParamProcessor.java new file mode 100644 index 0000000..700ea83 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultBodyParamProcessor.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.core.http.processor; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.http.annotation.BodyParam; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +public class DefaultBodyParamProcessor implements BodyParamProcessor { + @Override + public byte[] process(BodyParam bodyParam, String paramName, Class paramClazz, Object paramValue) { + Method toBytesMethod = ReflectUtil.getMethod(paramClazz, "toBytes"); + if (toBytesMethod == null) { + return paramValue.toString().getBytes(StandardCharsets.UTF_8); + } else { + return ReflectUtil.invoke(paramValue, toBytesMethod); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultFormBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultFormBodyParamProcessor.java new file mode 100644 index 0000000..07b3aeb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultFormBodyParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.FormBodyParam; + +import java.util.List; +import java.util.Map; + +public class DefaultFormBodyParamProcessor implements FormBodyParamProcessor { + @Override + public void process(FormBodyParam formBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultHeaderParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultHeaderParamProcessor.java new file mode 100644 index 0000000..b9bd685 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultHeaderParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.HeaderParam; + +import java.util.List; +import java.util.Map; + +public class DefaultHeaderParamProcessor implements HeaderParamProcessor { + @Override + public void process(HeaderParam headerParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultJsonBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultJsonBodyParamProcessor.java new file mode 100644 index 0000000..9a2cedd --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultJsonBodyParamProcessor.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.JsonBodyParam; +import com.njzscloud.common.core.jackson.Jackson; + +public class DefaultJsonBodyParamProcessor implements JsonBodyParamProcessor { + @Override + public byte[] process(JsonBodyParam jsonBodyParam, String paramName, Class paramClazz, Object paramValue) { + return Jackson.toJsonBytes(paramValue); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultMultiBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultMultiBodyParamProcessor.java new file mode 100644 index 0000000..43028b9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultMultiBodyParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.MultiBodyParam; +import com.njzscloud.common.core.tuple.Tuple3; + +import java.util.Map; + +public class DefaultMultiBodyParamProcessor implements MultiBodyParamProcessor { + @Override + public void process(MultiBodyParam multiBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultPathParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultPathParamProcessor.java new file mode 100644 index 0000000..cc470c4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultPathParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.PathParam; + +import java.util.Map; + +public class DefaultPathParamProcessor implements PathParamProcessor { + + @Override + public void process(PathParam pathParam, String paramName, Class paramClazz, Object paramValue, Map result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultQueryParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultQueryParamProcessor.java new file mode 100644 index 0000000..0e4c979 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultQueryParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.QueryParam; + +import java.util.List; +import java.util.Map; + +public class DefaultQueryParamProcessor implements QueryParamProcessor { + @Override + public void process(QueryParam queryParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultXmlBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultXmlBodyParamProcessor.java new file mode 100644 index 0000000..3b996fc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/DefaultXmlBodyParamProcessor.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.XmlBodyParam; +import com.njzscloud.common.core.jackson.Jackson; + +public class DefaultXmlBodyParamProcessor implements XmlBodyParamProcessor { + @Override + public byte[] process(XmlBodyParam xmlBodyParam, String paramName, Class paramClazz, Object paramValue) { + return Jackson.toXmlBytes(paramValue); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/FormBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/FormBodyParamProcessor.java new file mode 100644 index 0000000..9fcacb0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/FormBodyParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.FormBodyParam; + +import java.util.List; +import java.util.Map; + +public interface FormBodyParamProcessor { + void process(FormBodyParam formBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/HeaderParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/HeaderParamProcessor.java new file mode 100644 index 0000000..eb5576e --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/HeaderParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.HeaderParam; + +import java.util.List; +import java.util.Map; + +public interface HeaderParamProcessor { + void process(HeaderParam headerParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/JsonBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/JsonBodyParamProcessor.java new file mode 100644 index 0000000..3fceb40 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/JsonBodyParamProcessor.java @@ -0,0 +1,7 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.JsonBodyParam; + +public interface JsonBodyParamProcessor { + byte[] process(JsonBodyParam jsonBodyParam, String paramName, Class paramClazz, Object paramValue); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/MultiBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/MultiBodyParamProcessor.java new file mode 100644 index 0000000..ee3801b --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/MultiBodyParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.MultiBodyParam; +import com.njzscloud.common.core.tuple.Tuple3; + +import java.util.Map; + +public interface MultiBodyParamProcessor { + void process(MultiBodyParam multiBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/PathParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/PathParamProcessor.java new file mode 100644 index 0000000..7d05d5b --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/PathParamProcessor.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.PathParam; + +import java.util.Map; + +public interface PathParamProcessor { + void process(PathParam pathParam, String paramName, Class paramClazz, Object paramValue, Map result); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/QueryParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/QueryParamProcessor.java new file mode 100644 index 0000000..bad3a65 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/QueryParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.QueryParam; + +import java.util.List; +import java.util.Map; + +public interface QueryParamProcessor { + void process(QueryParam queryParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/XmlBodyParamProcessor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/XmlBodyParamProcessor.java new file mode 100644 index 0000000..d7ab83a --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/processor/XmlBodyParamProcessor.java @@ -0,0 +1,7 @@ +package com.njzscloud.common.core.http.processor; + +import com.njzscloud.common.core.http.annotation.XmlBodyParam; + +public interface XmlBodyParamProcessor { + byte[] process(XmlBodyParam xmlBodyParam, String paramName, Class paramClazz, Object paramValue); +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/BodyParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/BodyParamResolver.java new file mode 100644 index 0000000..f0745d1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/BodyParamResolver.java @@ -0,0 +1,43 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.http.annotation.BodyParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.http.processor.BodyParamProcessor; +import com.njzscloud.common.core.tuple.Tuple2; + +public class BodyParamResolver extends ParamResolver> { + byte[] result = null; + String contentType; + + public BodyParamResolver() { + super(BodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result == null) return null; + return Tuple2.create(contentType == null ? Mime.BINARY : contentType, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(BodyParam anno, String paramName, Class paramClazz, Object paramValue) { + contentType = anno.contentType(); + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, BodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (hasParameterAnno) return false; + + contentType = anno.contentType(); + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/FormBodyParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/FormBodyParamResolver.java new file mode 100644 index 0000000..0a69414 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/FormBodyParamResolver.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.http.annotation.FormBodyParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.http.processor.FormBodyParamProcessor; +import com.njzscloud.common.core.tuple.Tuple2; + +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class FormBodyParamResolver extends ParamResolver> { + Map> result = new HashMap<>(); + + public FormBodyParamResolver() { + super(FormBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return null; + StringJoiner joiner = new StringJoiner("&"); + result.forEach((k, v) -> v.forEach(it -> joiner.add(k + "=" + it))); + byte[] bytes = joiner.toString().getBytes(StandardCharsets.UTF_8); + return Tuple2.create(Mime.FORM, bytes); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(FormBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + String value = anno.value(); + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isArr(paramClazz)) resolveArr(StrUtil.isBlank(value) ? paramName : value, paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, FormBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != FormBodyParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + if (anno != null) { + String value = anno.value(); + if (StrUtil.isNotBlank(value)) paramName = value; + } + if (isArr(paramClazz)) resolveArr(paramName, paramValue); + else resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + Class clazz = v.getClass(); + String paramName = k.toString(); + if (isArr(clazz)) resolveArr(paramName, v); + else resolveFormable(null, paramName, clazz, v); + }); + } + + + protected void resolveFormable(FormBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.computeIfAbsent(key, it -> new ArrayList<>()).add(val); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/HeaderParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/HeaderParamResolver.java new file mode 100644 index 0000000..7b0ce38 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/HeaderParamResolver.java @@ -0,0 +1,92 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.http.annotation.HeaderParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.processor.DefaultHeaderParamProcessor; + +import java.math.RoundingMode; +import java.util.*; + +public class HeaderParamResolver extends ParamResolver> { + Map> result = new TreeMap<>(); + + public HeaderParamResolver() { + super(HeaderParam.class); + } + + @Override + public Map resolve(HttpMethod httpMethod, String urlTpl) { + HashMap map = new HashMap<>(); + result.forEach((k, v) -> map.put(k, String.join(",", v))); + return map; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(HeaderParam anno, String paramName, Class paramClazz, Object paramValue) { + String value = anno.value(); + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isArr(paramClazz)) resolveArr(StrUtil.isBlank(value) ? paramName : value, paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, HeaderParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != DefaultHeaderParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + if (anno != null) { + String value = anno.value(); + if (StrUtil.isNotBlank(value)) paramName = value; + } + if (isArr(paramClazz)) resolveArr(paramName, paramValue); + else resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + Class clazz = v.getClass(); + String paramName = k.toString(); + if (isArr(clazz)) resolveArr(paramName, v); + else resolveFormable(null, paramName, clazz, v); + }); + } + + + protected void resolveFormable(HeaderParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.computeIfAbsent(key, it -> new ArrayList<>()).add(val); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/JsonBodyParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/JsonBodyParamResolver.java new file mode 100644 index 0000000..d6a3c6c --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/JsonBodyParamResolver.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.http.annotation.JsonBodyParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.http.processor.JsonBodyParamProcessor; +import com.njzscloud.common.core.tuple.Tuple2; + +public class JsonBodyParamResolver extends ParamResolver> { + byte[] result = null; + + public JsonBodyParamResolver() { + super(JsonBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result == null) return null; + return Tuple2.create(Mime.JSON, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(JsonBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, JsonBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (hasParameterAnno) return false; + + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/MultiBodyParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/MultiBodyParamResolver.java new file mode 100644 index 0000000..f29f737 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/MultiBodyParamResolver.java @@ -0,0 +1,141 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.njzscloud.common.core.http.annotation.MultiBodyParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.http.processor.MultiBodyParamProcessor; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.tuple.Tuple3; + +import java.io.ByteArrayOutputStream; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class MultiBodyParamResolver extends ParamResolver> { + private static final byte[] DASHDASH = "--".getBytes(StandardCharsets.UTF_8); + private static final byte[] CRLF = StrUtil.CRLF.getBytes(StandardCharsets.UTF_8); + Map> result = new HashMap<>(); + + public MultiBodyParamResolver() { + super(MultiBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return null; + + String boundary = RandomUtil.randomString(16); + String contentType = Mime.MULTIPART_FORM + "; boundary=" + boundary; + byte[] bytes = buildContent(boundary.getBytes(StandardCharsets.UTF_8)); + return Tuple2.create(contentType, bytes); + } + + private byte[] buildContent(byte[] boundary) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + result.forEach((k, v) -> { + String filename = URLUtil.encode(v.get_0()); + String contentType = v.get_1(); + byte[] bytes = v.get_2(); + + IoUtil.write(byteArrayOutputStream, false, DASHDASH); + IoUtil.write(byteArrayOutputStream, false, boundary); + IoUtil.write(byteArrayOutputStream, false, CRLF); + + byte[] contentDisposition; + if (StrUtil.isNotBlank(filename)) { + contentDisposition = StrUtil.format("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"", k, filename).getBytes(StandardCharsets.UTF_8); + } else { + contentDisposition = StrUtil.format("Content-Disposition: form-data; name=\"{}\"", k).getBytes(StandardCharsets.UTF_8); + } + + IoUtil.write(byteArrayOutputStream, false, contentDisposition); + IoUtil.write(byteArrayOutputStream, false, CRLF); + + if (StrUtil.isNotBlank(contentType)) { + IoUtil.write(byteArrayOutputStream, false, ("Content-Type: " + contentType).getBytes(StandardCharsets.UTF_8)); + IoUtil.write(byteArrayOutputStream, false, CRLF); + } + + IoUtil.write(byteArrayOutputStream, false, CRLF); + IoUtil.write(byteArrayOutputStream, false, bytes); + IoUtil.write(byteArrayOutputStream, false, CRLF); + + }); + IoUtil.write(byteArrayOutputStream, false, DASHDASH); + IoUtil.write(byteArrayOutputStream, false, boundary); + IoUtil.write(byteArrayOutputStream, false, DASHDASH); + + return byteArrayOutputStream.toByteArray(); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(MultiBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz) || isBytes(paramClazz) || isIn(paramClazz) || isFile(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, MultiBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != MultiBodyParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + protected void resolveFormable(MultiBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + byte[] val; + String mime; + String filename = null; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + mime = Mime.TXT; + val = formatNum(format, roundingMode, paramValue).getBytes(StandardCharsets.UTF_8); + } else if (isDt(paramClazz)) { + mime = Mime.TXT; + val = formatDt(format, paramValue).getBytes(StandardCharsets.UTF_8); + } else if (isBytes(paramClazz)) { + mime = Mime.BINARY; + val = (byte[]) paramValue; + } else if (isIn(paramClazz) || isFile(paramClazz)) { + mime = Mime.BINARY; + Tuple2 fileInfo = toFileInfo(paramValue); + filename = fileInfo.get_0(); + val = fileInfo.get_1(); + } else { + mime = Mime.TXT; + val = paramValue.toString().getBytes(StandardCharsets.UTF_8); + } + + result.put(key, Tuple3.create(filename, mime, val)); + + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/ParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/ParamResolver.java new file mode 100644 index 0000000..b9f6ddb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/ParamResolver.java @@ -0,0 +1,242 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.tuple.Tuple3; +import lombok.RequiredArgsConstructor; + +import java.io.File; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.*; + +@RequiredArgsConstructor +public abstract class ParamResolver { + protected final Class annoType; + + abstract public R resolve(HttpMethod httpMethod, String urlTpl); + + public final void resolve(Parameter parameter, Object obj) { + if (obj == null) return; + + Class parameterClazz = obj.getClass(); + String parameterName; + T parameterAnno = null; + if (parameter == null) { + parameterName = StrUtil.lowerFirst(obj.getClass().getName()); + } else { + parameterName = parameter.getName(); + parameterAnno = parameter.getAnnotation(this.annoType); + } + + if (parameterAnno == null) { + parameterAnno = parameterClazz.getAnnotation(this.annoType); + } + + int state = 0; + + if (parameterAnno != null) { + state = this.resolveParameter(parameterAnno, parameterName, parameterClazz, obj) ? + 1 : + 2; + } + + if (state == 1) return; + + List getterMethods = ReflectUtil.getPublicMethods(parameterClazz, + it -> (it.getName().startsWith("get") || it.getName().startsWith("is")) && + !Modifier.isStatic(it.getModifiers()) && + !it.getName().equals("getClass") + ); + + for (Method getterMethod : getterMethods) { + Object fieldValue = ReflectUtil.invoke(obj, getterMethod); + if (fieldValue == null) continue; + String getterMethodName = getterMethod.getName(); + Class returnType = getterMethod.getReturnType(); + String fieldName = StrUtil.lowerFirst(getterMethodName.startsWith("get") ? + getterMethodName.substring(3) : + getterMethodName.substring(2)); + + T fieldAnno = getterMethod.getAnnotation(this.annoType); + if (fieldAnno == null) { + Field field = ReflectUtil.getField(parameterClazz, fieldName); + fieldAnno = field.getAnnotation(this.annoType); + } + + if (fieldAnno == null && state == 0) continue; + + this.resolveField(state == 2, fieldAnno, fieldName, returnType, fieldValue); + } + } + + public final void resolve(Object obj) { + resolve(null, obj); + } + + protected abstract boolean resolveParameter(T anno, String paramName, Class paramClazz, Object paramValue); + + protected abstract boolean resolveField(boolean hasParameterAnno, T anno, String paramName, Class paramClazz, Object paramValue); + + + public final boolean isNum(Class clazz) { + return clazz == byte.class || clazz == short.class || + clazz == int.class || clazz == long.class || + clazz == float.class || clazz == double.class + || Number.class.isAssignableFrom(clazz); + } + + protected final boolean isStr(Class clazz) { + return clazz == String.class || clazz == char.class || clazz == Character.class; + } + + protected final boolean isDt(Class clazz) { + return clazz == LocalDate.class || + clazz == LocalTime.class || + clazz == LocalDateTime.class || + Date.class.isAssignableFrom(clazz); + } + + protected final boolean isArr(Class clazz) { + return Collection.class.isAssignableFrom(clazz) || clazz.isArray(); + } + + protected final Collection toArr(Object obj) { + if (obj == null) return null; + Class clazz = obj.getClass(); + if (clazz.isArray()) { + return Arrays.asList((Object[]) obj); + } else if (Collection.class.isAssignableFrom(clazz)) { + return (Collection) obj; + } else { + throw new RuntimeException("不是数组"); + } + } + + protected final boolean isKv(Class clazz) { + return Map.class.isAssignableFrom(clazz); + } + + protected final boolean isIn(Class clazz) { + return InputStream.class.isAssignableFrom(clazz); + } + + protected final boolean isBytes(Class clazz) { + return clazz == byte[].class || clazz == Byte[].class; + } + + protected final Tuple2 toFileInfo(Object obj) { + if (obj == null) return null; + Class clazz = obj.getClass(); + if (isIn(clazz)) { + byte[] bytes = IoUtil.readBytes((InputStream) obj); + return Tuple3.create(null, bytes); + } else if (isFile(clazz)) { + byte[] bytes = FileUtil.readBytes((File) obj); + String name = ((File) obj).getName(); + return Tuple3.create(name, bytes); + } else { + throw new RuntimeException("不是文件"); + } + } + + protected final boolean isFile(Class clazz) { + return File.class.isAssignableFrom(clazz); + } + + + protected final String formatDt(String format, Object value) { + if (value == null) return null; + Class clazz = value.getClass(); + if (Date.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_DATETIME_PATTERN; + return DateUtil.format(((Date) value), format); + } else if (LocalDateTime.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_DATETIME_PATTERN; + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTimeFormatter.format((TemporalAccessor) value); + } else if (LocalDate.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_DATE_PATTERN; + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTimeFormatter.format((TemporalAccessor) value); + } else if (LocalTime.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_TIME_PATTERN; + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTimeFormatter.format((TemporalAccessor) value); + } else { + throw new RuntimeException("不是时间"); + } + } + + protected final String formatDt(Object value) { + return formatDt(null, value); + } + + protected final String formatNum(String format, RoundingMode roundingMode, Object value) { + if (value == null) return null; + if (StrUtil.isBlank(format)) return formatNum(value); + + Class clazz = value.getClass(); + if (isNum(clazz)) { + return NumberUtil.decimalFormat(format, value, roundingMode); + } else { + throw new RuntimeException("不是数字"); + } + } + + protected final String formatNum(String format, Object value) { + return formatNum(format, null, value); + } + + protected final String formatNum(Object value) { + if (value == null) return null; + + Class clazz = value.getClass(); + if (isNum(clazz)) { + if (clazz == BigDecimal.class) { + return ((BigDecimal) value).toPlainString(); + } else { + return value.toString(); + } + } else { + throw new RuntimeException("不是数字"); + } + } + + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + resolveFormable(null, k.toString(), v.getClass(), v); + }); + } + + protected void resolveArr(String paramName, Object paramValue) { + toArr(paramValue) + .stream() + .filter(Objects::nonNull) + .forEach(it -> resolveFormable(null, paramName, it.getClass(), it)); + } + + protected void resolveFormable(T anno, String paramName, Class paramClazz, Object paramValue) { + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/PathParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/PathParamResolver.java new file mode 100644 index 0000000..064b1d1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/PathParamResolver.java @@ -0,0 +1,74 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.http.annotation.PathParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.processor.PathParamProcessor; + +import java.math.RoundingMode; +import java.util.Map; +import java.util.TreeMap; + +public class PathParamResolver extends ParamResolver { + Map result = new TreeMap<>(); + + public PathParamResolver() { + super(PathParam.class); + } + + @Override + public String resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return urlTpl; + return StrUtil.format(urlTpl, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(PathParam anno, String paramName, Class paramClazz, Object paramValue) { + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, PathParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != PathParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + protected void resolveFormable(PathParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.put(key, val); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/QueryParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/QueryParamResolver.java new file mode 100644 index 0000000..15abff5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/QueryParamResolver.java @@ -0,0 +1,92 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.http.annotation.QueryParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.processor.DefaultQueryParamProcessor; + +import java.math.RoundingMode; +import java.util.*; + +public class QueryParamResolver extends ParamResolver { + Map> result = new TreeMap<>(); + + public QueryParamResolver() { + super(QueryParam.class); + } + + @Override + public String resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return urlTpl; + StringJoiner joiner = new StringJoiner("&", urlTpl + (urlTpl.contains("?") ? "&" : "?"), ""); + result.forEach((k, v) -> v.forEach(it -> joiner.add(k + "=" + it))); + return joiner.toString(); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(QueryParam anno, String paramName, Class paramClazz, Object paramValue) { + String value = anno.value(); + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isArr(paramClazz)) resolveArr(StrUtil.isBlank(value) ? paramName : value, paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, QueryParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != DefaultQueryParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + if (anno != null) { + String value = anno.value(); + if (StrUtil.isNotBlank(value)) paramName = value; + } + if (isArr(paramClazz)) resolveArr(paramName, paramValue); + else resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + Class clazz = v.getClass(); + String paramName = k.toString(); + if (isArr(clazz)) resolveArr(paramName, v); + else resolveFormable(null, paramName, clazz, v); + }); + } + + + protected void resolveFormable(QueryParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.computeIfAbsent(key, it -> new ArrayList<>()).add(val); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/XmlBodyParamResolver.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/XmlBodyParamResolver.java new file mode 100644 index 0000000..64670d1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/resolver/XmlBodyParamResolver.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.core.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.http.annotation.XmlBodyParam; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.http.processor.XmlBodyParamProcessor; +import com.njzscloud.common.core.tuple.Tuple2; + +public class XmlBodyParamResolver extends ParamResolver> { + byte[] result = null; + + public XmlBodyParamResolver() { + super(XmlBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result == null) return null; + + return Tuple2.create(Mime.XML, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(XmlBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, XmlBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (hasParameterAnno) return false; + + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ParameterInfo.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ParameterInfo.java new file mode 100644 index 0000000..fcd7aae --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ParameterInfo.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.http.support; + +import java.lang.annotation.Annotation; +import java.util.List; + +public class ParameterInfo { + private String name; + private List annoList; + private Object value; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/RequestInfo.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/RequestInfo.java new file mode 100644 index 0000000..23009fc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/RequestInfo.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.core.http.support; + +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.http.constant.HttpMethod; +import com.njzscloud.common.core.tuple.Tuple2; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class RequestInfo { + public final String desc; + public final HttpMethod method; + public final String url; + public final Map headers; + public final String contentType; + public final byte[] body; + + public static RequestInfo create(String desc, HttpMethod method, String url, Map headers, Tuple2 body) { + String contentType = null; + byte[] bytes = null; + if (body != null) { + contentType = body.get_0(); + bytes = body.get_1(); + } + return new RequestInfo(desc, method, url, MapUtil.unmodifiable(headers), contentType, bytes); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseInfo.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseInfo.java new file mode 100644 index 0000000..0719f98 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseInfo.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.core.http.support; + +import cn.hutool.core.map.MapUtil; + +import java.util.List; +import java.util.Map; + +public class ResponseInfo { + public final boolean success; + public final int code; + public final String message; + public final Map> header; + public final byte[] body; + public final Exception e; + + public ResponseInfo(boolean success, int code, String message, Map> header, byte[] body, Exception e) { + this.success = success; + this.code = code; + this.message = message; + this.header = header; + this.body = body; + this.e = e; + } + + public static ResponseInfo create(int code, String message, Map> header, byte[] body) { + return new ResponseInfo(code >= 200 && code <= 299, code, message, header, body, null); + } + + public static ResponseInfo create(Exception e) { + return new ResponseInfo(false, 0, e.getMessage(), MapUtil.empty(), new byte[0], e); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseResult.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseResult.java new file mode 100644 index 0000000..48a7a14 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/http/support/ResponseResult.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.core.http.support; + +import java.util.List; +import java.util.Map; + +public final class ResponseResult { + /** + * 是否成功, HTTP 状态码 200..299 + */ + public final boolean success; + + /** + * HTTP 状态码 + */ + public final int code; + + /** + * HTTP 状态信息 + */ + public final String status; + + /** + * HTTP 响应头 + */ + public final Map> headers; + + /** + * HTTP 响应体 + */ + public final T body; + + /** + * 错误信息, 在请求发生异常时有值 + */ + public final Exception e; + + private ResponseResult(boolean success, int code, String status, Map> headers, T body, Exception e) { + this.success = success; + this.code = code; + this.status = status; + this.headers = headers; + this.body = body; + this.e = e; + } + + public static ResponseResultBuilder builder() { + return new ResponseResultBuilder(); + } + + public static class ResponseResultBuilder { + private int code; + private String status; + private Map> headers; + private T body; + private Exception e; + + private ResponseResultBuilder() { + } + + public ResponseResultBuilder code(int code) { + this.code = code; + return this; + } + + public ResponseResultBuilder status(String status) { + this.status = status; + return this; + } + + public ResponseResultBuilder headers(Map> headers) { + this.headers = headers; + return this; + } + + public ResponseResultBuilder body(T body) { + this.body = body; + return this; + } + + public ResponseResultBuilder e(Exception e) { + this.e = e; + return this; + } + + public ResponseResult build() { + return new ResponseResult<>( + code >= 200 && code <= 299, + code, + status, + headers, + body, + e + ); + } + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java new file mode 100644 index 0000000..5b4bfe2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java @@ -0,0 +1,70 @@ +package com.njzscloud.common.core.ienum; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.njzscloud.common.core.fastjson.serializer.DictObjectDeserializer; +import com.njzscloud.common.core.fastjson.serializer.DictObjectSerializer; +import com.njzscloud.common.core.jackson.serializer.DictDeserializer; +import com.njzscloud.common.core.jackson.serializer.DictSerializer; + +/** + * 字典枚举
+ * 仅两个子接口 DictInt、DictStr
+ * + * @see DictInt + * @see DictStr + * @see DictDeserializer + * @see DictSerializer + * @see DictObjectDeserializer + * @see DictObjectSerializer + */ +@JsonDeserialize(using = DictDeserializer.class) +@JsonSerialize(using = DictSerializer.class) +public interface Dict extends IEnum { + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 val 属性值 + */ + String ENUM_VAL = "val"; + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 txt 属性值 + */ + String ENUM_TXT = "txt"; + + /** + * 根据 "值" 获取到对应的枚举对象 + * + * @param val 值 + * @param ds 枚举对象数组 + * @param 值类型 + * @param 枚举类型 + * @return Dict + */ + static > D parse(V val, D[] ds) { + for (D d : ds) { + if (d.getVal().equals(val)) { + return d; + } + } + return null; + } + + /** + * 值 + * + * @return T + */ + T getVal(); + + /** + * 文本表示 + * + * @return String + */ + String getTxt(); + + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java new file mode 100644 index 0000000..6e9c5cb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.core.ienum; + + +/** + * "值" 类型为 Integer
+ * 枚举应实现此接口 + * + * @see DictStr + */ +public interface DictInt extends Dict { +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java new file mode 100644 index 0000000..345cfac --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.ienum; + +/** + * "值" 类型为 String
+ * 枚举应实现此接口 + * + * @see DictInt + */ +public interface DictStr extends Dict { +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java new file mode 100644 index 0000000..7bf9bb5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.core.ienum; + +/** + * 枚举接口 + */ +public interface IEnum { + /** + * 枚举单独序列化时的属性
+ * 存放枚举的全限定类名 + */ + String ENUM_TYPE = "type"; + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 name 属性值 + */ + String ENUM_NAME = "name"; + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 ordinal 属性值 + */ + String ENUM_ORDINAL = "ordinal"; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java new file mode 100644 index 0000000..0405e00 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java @@ -0,0 +1,229 @@ +package com.njzscloud.common.core.jackson; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.extra.spring.SpringUtil; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.serializer.BigDecimalModule; +import com.njzscloud.common.core.jackson.serializer.LongModule; +import com.njzscloud.common.core.jackson.serializer.TimeModule; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStream; +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Jackson 工具
+ * 从 Spring 中获取 ObjectMapper + */ +@Slf4j +public class Jackson { + private static final ObjectMapper objectMapper; + private static final XmlMapper xmlMapper; + + static { + ObjectMapper temp; + try { + temp = SpringUtil.getBean(ObjectMapper.class); + } catch (Throwable e) { + log.warn("从 Spring 中获取 ObjectMapper 失败"); + temp = createObjectMapper(); + } + objectMapper = temp; + XmlMapper _xmlMapper; + try { + _xmlMapper = SpringUtil.getBean(XmlMapper.class); + } catch (Throwable e) { + log.warn("从 Spring 中获取 XmlMapper 失败"); + _xmlMapper = createXmlMapper(); + } + xmlMapper = _xmlMapper; + } + + + /** + * 序列化为 JSON 字符串 + * + * @param o 数据 + * @return String + */ + public static String toJsonStr(Object o) { + try { + return objectMapper.writeValueAsString(o); + } catch (JsonProcessingException e) { + log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + /** + * 序列化为 JSON 字节数组 + * + * @param o 数据 + * @return byte[] + */ + public static byte[] toJsonBytes(Object o) { + try { + return objectMapper.writeValueAsBytes(o); + } catch (JsonProcessingException e) { + log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + /** + * 反序列化(泛型支持)
+ * 如: new TypeReference<List<String>>(){}, 类型对象建议缓存 + * + * @param json json JSON 字符串 + * @param type 类型 + * @return T + * @see TypeReference + */ + public static T toBean(String json, Type type) { + try { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type)); + } catch (JsonProcessingException e) { + log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T toBean(InputStream json, Type type) { + try { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T toBean(byte[] json, Type type) { + try { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + + /** + * 序列化为 JSON 字符串 + * + * @param o 数据 + * @return String + */ + public static String toXmlStr(Object o) { + try { + return xmlMapper.writeValueAsString(o); + } catch (JsonProcessingException e) { + log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + /** + * 序列化为 JSON 字节数组 + * + * @param o 数据 + * @return byte[] + */ + public static byte[] toXmlBytes(Object o) { + try { + return xmlMapper.writeValueAsBytes(o); + } catch (JsonProcessingException e) { + log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + public static T xmlToBean(String json, Type type) { + try { + return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type)); + } catch (JsonProcessingException e) { + log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T xmlToBean(InputStream json, Type type) { + try { + return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T xmlToBean(byte[] json, Type type) { + try { + return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + /** + * 创建 ObjectMapper + * + * @return ObjectMapper + */ + private static ObjectMapper createObjectMapper() { + return new ObjectMapper() + .setLocale(Locale.CHINA) + .setTimeZone(TimeZone.getTimeZone("GMT+8")) + .setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN)) + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .configure(SerializationFeature.INDENT_OUTPUT, true) // 格式化输出 + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true) + .configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true) + .registerModules(new TimeModule(), new LongModule(), new BigDecimalModule()) + ; + } + + + private static XmlMapper createXmlMapper() { + return XmlMapper.xmlBuilder() + .defaultLocale(Locale.CHINA) + .defaultTimeZone(TimeZone.getTimeZone("GMT+8")) + .defaultDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN)) + .serializationInclusion(JsonInclude.Include.ALWAYS) + .configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .addModules(new TimeModule(), new LongModule(), new BigDecimalModule()) + .build() + ; + } + + /** + * 获取 objectMapper + * + * @return ObjectMapper + */ + public static ObjectMapper objectMapper() { + return objectMapper; + } + + public static XmlMapper xmlMapper() { + return xmlMapper; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java new file mode 100644 index 0000000..dd4d211 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.math.BigDecimal; + +/** + * BigDecimal 序列化 + */ +public class BigDecimalModule extends SimpleModule { + { + this.addSerializer(BigDecimal.class, new BigDecimalSerializer()) + .addDeserializer(BigDecimal.class, new NumberDeserializers.BigDecimalDeserializer()); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java new file mode 100644 index 0000000..216e1f2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * BigDecimal 序列化为字符串 + */ +public class BigDecimalSerializer extends JsonSerializer { + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(value.toPlainString()); + } else { + gen.writeNull(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java new file mode 100644 index 0000000..c7dc002 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java @@ -0,0 +1,102 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Field; + +/** + * Dict 枚举的 Jackson 反序列化器

+ * JSON 格式见对应的序列化器 {@link DictSerializer} + * + * @see Dict + * @see DictInt + * @see DictStr + * @see DictSerializer + */ +@Slf4j +public class DictDeserializer extends JsonDeserializer { + private static final ClassLoader CLASSLOADER = DictDeserializer.class.getClassLoader(); + + @Override + public Dict deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + JsonToken currentToken = p.getCurrentToken(); + + if (currentToken == JsonToken.START_OBJECT) { + TreeNode treeNode = p.getCodec().readTree(p); + TreeNode enumType_node = treeNode.get(IEnum.ENUM_TYPE); + String enumTypeField = ((TextNode) enumType_node).textValue(); + TreeNode val_node = treeNode.get(Dict.ENUM_VAL); + + Class clazz; + try { + clazz = CLASSLOADER.loadClass(enumTypeField); + } catch (ClassNotFoundException e) { + throw Exceptions.error(e, "类型加载失败:{}", enumTypeField); + } + if (val_node instanceof TextNode) { + if (DictStr.class.isAssignableFrom(clazz)) { + String val = ((TextNode) val_node).textValue(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + String val = ((TextNode) val_node).textValue(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(Integer.parseInt(val), constants); + } else { + return null; + } + } else if (val_node instanceof IntNode) { + if (DictStr.class.isAssignableFrom(clazz)) { + int val = ((IntNode) val_node).intValue(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(String.valueOf(val), constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + int val = ((IntNode) val_node).intValue(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else { + return null; + } + } else { + return null; + } + + } else { + JsonStreamContext context = p.getParsingContext(); + String currentName = context.getCurrentName(); + + Object currentValue = p.getCurrentValue(); + + Class valueClazz = currentValue.getClass(); + try { + Field field = valueClazz.getDeclaredField(currentName); + Class clazz = field.getType(); + if (DictStr.class.isAssignableFrom(clazz)) { + String val = p.getValueAsString(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + int val = p.getValueAsInt(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else { + return null; + } + } catch (Exception e) { + log.error("字典枚举反序列化失败", e); + throw Exceptions.error(e, "字典枚举反序列化失败,字段名:{},值:{}", currentName, currentValue); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java new file mode 100644 index 0000000..cc4b4ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java @@ -0,0 +1,61 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; + +import java.io.IOException; + +/** + * Dict 枚举的 Jackson 序列化器

+ * JSON 格式
+ * 1、枚举不是其他对象的属性
+ *
+ * {
+ *   "type": "", // 枚举全限定类名, 反序列化时会用到
+ *   "name": "", // name 属性
+ *   "ordinal": 0, // ordinal 属性
+ *   "val": 1,  // val 属性(字符串/数字), 反序列化时会用到
+ *   "txt": "1" // txt 属性
+ * }
+ * 2、枚举是其他对象的属性
+ *
+ * {
+ *   // ... 其他属性
+ *   "原字段名称": 1, // val 属性(字符串/数字), 反序列化时会用到
+ *   "原字段名称Txt": "1" //  txt 属性
+ * }
+ * + * @see Dict + * @see DictInt + * @see DictStr + * @see DictDeserializer + */ +public class DictSerializer extends JsonSerializer { + @Override + public void serialize(Dict value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + JsonStreamContext ctx = gen.getOutputContext(); + if (ctx.inRoot()) { + gen.writeStartObject(); + gen.writeStringField(IEnum.ENUM_TYPE, value.getClass().getName()); + gen.writeStringField(IEnum.ENUM_NAME, ((Enum) value).name()); + gen.writeNumberField(IEnum.ENUM_ORDINAL, ((Enum) value).ordinal()); + gen.writeObjectField(Dict.ENUM_VAL, value.getVal()); + gen.writeStringField(Dict.ENUM_TXT, value.getTxt()); + gen.writeEndObject(); + } else { + gen.writeObject(value.getVal()); + String currentName = ctx.getCurrentName(); + gen.writeStringField(currentName + "Txt", value.getTxt()); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java new file mode 100644 index 0000000..e960706 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; + + +/** + * Long 序列化 + */ +public class LongModule extends SimpleModule { + { + this.addSerializer(Long.class, new LongSerializer()) + .addDeserializer(Long.class, new NumberDeserializers.LongDeserializer(Long.class, null)); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java new file mode 100644 index 0000000..c54a7ad --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * Long 序列化为字符串 + */ +public class LongSerializer extends JsonSerializer { + @Override + public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(value.toString()); + } else { + gen.writeNull(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java new file mode 100644 index 0000000..adb18d4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.core.jackson.serializer; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * 时间类型序列化 + */ +public class TimeModule extends SimpleModule { + { + this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) + .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) + .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))) + .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) + .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) + .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java new file mode 100644 index 0000000..93aca4b --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java @@ -0,0 +1,688 @@ +package com.njzscloud.common.core.thread; + +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +@Slf4j +@SuppressWarnings("unchecked") +public class Q extends AbstractQueue implements BlockingQueue { + private final ReentrantLock takeLock = new ReentrantLock(); + private final Condition notEmpty = takeLock.newCondition(); + private final ReentrantLock putLock = new ReentrantLock(); + private final Condition notFull = putLock.newCondition(); + + private final int capacity; + private final int standbyCapacity; + private final AtomicInteger count = new AtomicInteger(); + private final AtomicInteger standbyCount = new AtomicInteger(); + + private Node head; + private Node last; + private Node border; + + + public Q() { + this(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public Q(int capacity, int standbyCapacity) { + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + this.standbyCapacity = standbyCapacity; + border = last = head = new Node<>(null); + } + + private void signalNotEmpty() { + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + } + + private void signalNotFull() { + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + notFull.signal(); + } finally { + putLock.unlock(); + } + } + + private void enqueue(Node node) { + last = last.next = node; + } + + private E dequeue() { + Node h = head; + Node first = h.next; + h.next = h; // help GC + head = first; + E x = first.item; + first.item = null; + return x; + } + + private void fullyLock() { + putLock.lock(); + takeLock.lock(); + } + + private void fullyUnlock() { + takeLock.unlock(); + putLock.unlock(); + } + + public int size() { + return count.get(); + } + + public int remainingCapacity() { + return capacity - count.get(); + } + + public void put(E e) throws InterruptedException { + if (e == null) throw new NullPointerException(); + int c = -1; + Node node = new Node(e); + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.count; + putLock.lockInterruptibly(); + try { + + while (count.get() == capacity) { + notFull.await(); + } + enqueue(node); + border = node; + c = count.getAndIncrement(); + if (c + 1 < capacity) notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) signalNotEmpty(); + } + + public boolean offer(E e, long timeout, TimeUnit unit) + throws InterruptedException { + + if (e == null) throw new NullPointerException(); + long nanos = unit.toNanos(timeout); + int c = -1; + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.count; + putLock.lockInterruptibly(); + try { + while (count.get() == capacity) { + if (nanos <= 0) + return false; + nanos = notFull.awaitNanos(nanos); + } + Node node = new Node<>(e); + enqueue(node); + border = node; + c = count.getAndIncrement(); + if (c + 1 < capacity) notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) + signalNotEmpty(); + return true; + } + + + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + final AtomicInteger count = this.count; + if (count.get() == capacity) return false; + int c = -1; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + if (count.get() < capacity) { + enqueue(node); + border = node; + c = count.getAndIncrement(); + if (c + 1 < capacity) notFull.signal(); + } + // log.info("放1:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + putLock.unlock(); + } + if (c == 0) signalNotEmpty(); + return c >= 0; + } + + public boolean offerStandby(E e) { + if (e == null) throw new NullPointerException(); + final AtomicInteger count = this.count; + if (standbyCount.get() == standbyCapacity) return false; + int c = -1; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + if (standbyCount.get() < standbyCapacity) { + enqueue(node); + c = count.get(); + if (c == capacity) { + c--; + standbyCount.getAndIncrement(); + } else { + border = border.next; + c = count.getAndIncrement(); + } + if (c + 1 < capacity) notFull.signal(); + } + // log.info("放2:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + putLock.unlock(); + } + if (c == 0) signalNotEmpty(); + return c >= 0; + } + + public E take() throws InterruptedException { + E x; + int c = -1; + final AtomicInteger count = this.count; + final ReentrantLock takeLock = this.takeLock; + takeLock.lockInterruptibly(); + try { + while (count.get() == 0) { + notEmpty.await(); + } + if (border.next == null) { + c = count.getAndDecrement(); + } else { + border = border.next; + standbyCount.getAndDecrement(); + c = count.get(); + } + x = dequeue(); + if (c > 1) notEmpty.signal(); + // log.info("取2:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + takeLock.unlock(); + } + if (c == capacity) signalNotFull(); + return x; + } + + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + E x = null; + int c = -1; + long nanos = unit.toNanos(timeout); + final AtomicInteger count = this.count; + final ReentrantLock takeLock = this.takeLock; + takeLock.lockInterruptibly(); + try { + while (count.get() == 0) { + if (nanos <= 0) + return null; + nanos = notEmpty.awaitNanos(nanos); + } + if (border.next == null) { + c = count.getAndDecrement(); + } else { + border = border.next; + standbyCount.getAndDecrement(); + c = count.get(); + } + x = dequeue(); + if (c > 1) notEmpty.signal(); + // log.info("取1:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + takeLock.unlock(); + } + if (c == capacity) + signalNotFull(); + return x; + } + + public E poll() { + final AtomicInteger count = this.count; + if (count.get() == 0) + return null; + E x = null; + int c = -1; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + if (count.get() > 0) { + if (border.next == null) { + c = count.getAndDecrement(); + } else { + border = border.next; + standbyCount.getAndDecrement(); + c = count.get(); + } + x = dequeue(); + if (c > 1) notEmpty.signal(); + } + } finally { + takeLock.unlock(); + } + if (c == capacity) + signalNotFull(); + return x; + } + + public E peek() { + if (count.get() == 0) return null; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + Node first = head.next; + if (first == null) return null; + else return first.item; + } finally { + takeLock.unlock(); + } + } + + void unlink(Node trail, Node p, int borderFlag) { + p.item = null; + trail.next = p.next; + if (last == p) last = trail; + if (borderFlag == 0) { + if (last == p) { + border = trail; + if (count.getAndDecrement() == capacity) notFull.signal(); + } else { + border = p.next; + } + } else if (borderFlag == -1) { + if (last == border) { + if (count.getAndDecrement() == capacity) notFull.signal(); + } else { + border = border.next; + } + } else if (borderFlag == 1) { + standbyCount.getAndDecrement(); + } + } + + public boolean remove(Object o) { + if (o == null) return false; + fullyLock(); + try { + // -1-->左边、0-->边界、1-->右边 + int borderFlag = -1; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (p == border) { + borderFlag = 0; + } else if (borderFlag == 0) { + borderFlag = 1; + } + if (o.equals(p.item)) { + unlink(trail, p, borderFlag); + return true; + } + } + return false; + } finally { + fullyUnlock(); + } + } + + public boolean contains(Object o) { + if (o == null) return false; + fullyLock(); + try { + for (Node p = head.next; p != null; p = p.next) + if (o.equals(p.item)) + return true; + return false; + } finally { + fullyUnlock(); + } + } + + public Object[] toArray() { + fullyLock(); + try { + int size = count.get() + standbyCount.get(); + Object[] a = new Object[size]; + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = p.item; + return a; + } finally { + fullyUnlock(); + } + } + + public T[] toArray(T[] a) { + fullyLock(); + try { + int size = count.get() + standbyCount.get(); + if (a.length < size) + a = (T[]) java.lang.reflect.Array.newInstance + (a.getClass().getComponentType(), size); + + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = (T) p.item; + if (a.length > k) + a[k] = null; + return a; + } finally { + fullyUnlock(); + } + } + + public String toString() { + fullyLock(); + try { + Node p = head.next; + if (p == null) + return "[]"; + + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (; ; ) { + E e = p.item; + sb.append(e == this ? "(this Collection)" : e); + p = p.next; + if (p == null) + return sb.append(']').toString(); + sb.append(',').append(' '); + } + } finally { + fullyUnlock(); + } + } + + public void clear() { + fullyLock(); + try { + for (Node p, h = head; (p = h.next) != null; h = p) { + h.next = h; + p.item = null; + } + head = last; + // assert head.item == null && head.next == null; + standbyCount.getAndSet(0); + if (count.getAndSet(0) == capacity) + notFull.signal(); + } finally { + fullyUnlock(); + } + } + + public int drainTo(Collection c) { + return drainTo(c, Integer.MAX_VALUE); + } + + public int drainTo(Collection c, int maxElements) { + if (c == null) + throw new NullPointerException(); + if (c == this) + throw new IllegalArgumentException(); + if (maxElements <= 0) return 0; + boolean signalNotFull = false; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + int n = Math.min(maxElements, count.get() + standbyCount.get()); + // count.get provides visibility to first n Nodes + Node h = head; + int i = 0; + int x = 0; + int y = 0; + try { + while (i < n) { + Node p = h.next; + c.add(p.item); + if (border == last) { + x++; + } else { + border = border.next; + y++; + } + p.item = null; + h.next = h; + h = p; + ++i; + } + return n; + } finally { + head = h; + if (x > 0) { + signalNotFull = (count.getAndAdd(-x) == capacity); + } + if (y > 0) { + standbyCount.getAndAdd(-y); + } + } + } finally { + takeLock.unlock(); + if (signalNotFull) + signalNotFull(); + } + } + + public Iterator iterator() { + return new Itr(); + } + + public Spliterator spliterator() { + return new LBQSpliterator(this); + } + + static class Node { + E item; + + Node next; + + Node(E x) { + item = x; + } + } + + static final class LBQSpliterator implements Spliterator { + static final int MAX_BATCH = 1 << 25; // max batch array size; + final Q queue; + Node current; // current node; null until initialized + int batch; // batch size for splits + boolean exhausted; // true when no more nodes + long est; // size estimate + + LBQSpliterator(Q queue) { + this.queue = queue; + this.est = queue.size(); + } + + public long estimateSize() { + return est; + } + + public Spliterator trySplit() { + Node h; + final Q q = this.queue; + int b = batch; + int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1; + if (!exhausted && + ((h = current) != null || (h = q.head.next) != null) && + h.next != null) { + Object[] a = new Object[n]; + int i = 0; + Node p = current; + q.fullyLock(); + try { + if (p != null || (p = q.head.next) != null) { + do { + if ((a[i] = p.item) != null) + ++i; + } while ((p = p.next) != null && i < n); + } + } finally { + q.fullyUnlock(); + } + if ((current = p) == null) { + est = 0L; + exhausted = true; + } else if ((est -= i) < 0L) + est = 0L; + if (i > 0) { + batch = i; + return Spliterators.spliterator + (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT); + } + } + return null; + } + + public void forEachRemaining(Consumer action) { + if (action == null) throw new NullPointerException(); + final Q q = this.queue; + if (!exhausted) { + exhausted = true; + Node p = current; + do { + E e = null; + q.fullyLock(); + try { + if (p == null) + p = q.head.next; + while (p != null) { + e = p.item; + p = p.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (e != null) + action.accept(e); + } while (p != null); + } + } + + public boolean tryAdvance(Consumer action) { + if (action == null) throw new NullPointerException(); + final Q q = this.queue; + if (!exhausted) { + E e = null; + q.fullyLock(); + try { + if (current == null) + current = q.head.next; + while (current != null) { + e = current.item; + current = current.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (current == null) + exhausted = true; + if (e != null) { + action.accept(e); + return true; + } + } + return false; + } + + public int characteristics() { + return Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT; + } + } + + private class Itr implements Iterator { + + private Node current; + private Node lastRet; + private E currentElement; + + Itr() { + fullyLock(); + try { + current = head.next; + if (current != null) + currentElement = current.item; + } finally { + fullyUnlock(); + } + } + + public boolean hasNext() { + return current != null; + } + + private Node nextNode(Node p) { + for (; ; ) { + Node s = p.next; + if (s == p) + return head.next; + if (s == null || s.item != null) + return s; + p = s; + } + } + + public E next() { + fullyLock(); + try { + if (current == null) + throw new NoSuchElementException(); + E x = currentElement; + lastRet = current; + current = nextNode(current); + currentElement = (current == null) ? null : current.item; + return x; + } finally { + fullyUnlock(); + } + } + + public void remove() { + if (lastRet == null) + throw new IllegalStateException(); + fullyLock(); + try { + Node node = lastRet; + lastRet = null; + // -1-->左边、0-->边界、1-->右边 + int borderFlag = -1; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (p == border) { + borderFlag = 0; + } else if (borderFlag == 0) { + borderFlag = 1; + } + if (p == node) { + unlink(trail, p, borderFlag); + break; + } + } + } finally { + fullyUnlock(); + } + } + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java new file mode 100644 index 0000000..801a93c --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java @@ -0,0 +1,138 @@ +package com.njzscloud.common.core.thread; + + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class ThreadPool { + private static volatile ThreadPoolExecutor THREAD_POOL; + + public static ThreadPoolExecutor defaultThreadPool() { + if (THREAD_POOL == null) { + synchronized (ThreadPool.class) { + if (THREAD_POOL == null) { + THREAD_POOL = createThreadPool(null, + 10, 200, + 5 * 60, + 20, 2000, + null); + } + } + } + return THREAD_POOL; + } + + public static ThreadPoolExecutor createThreadPool(String poolName, int corePoolSize, int maxPoolSize, long keepAliveSeconds, int windowCapacity, int standbyCapacity, RejectedExecutionHandler abortPolicy) { + RejectedExecutionHandler abortPolicy_ = abortPolicy == null ? new ThreadPoolExecutor.AbortPolicy() : abortPolicy; + Q q = new Q<>(windowCapacity, standbyCapacity); + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveSeconds, TimeUnit.SECONDS, + q, + new DefaultThreadFactory(poolName), + (r, p) -> { + if (!q.offerStandby(r)) { + log.debug("任务队列已满"); + abortPolicy_.rejectedExecution(r, p); + } else { + log.debug("任务已加入备用队列"); + } + } + ); + threadPoolExecutor.allowCoreThreadTimeOut(true); + return threadPoolExecutor; + } + + public static void main(String[] args) { + ThreadPoolExecutor threadPool = defaultThreadPool(); + try { + threadPool.execute(() -> { + System.out.println("1--->" + Thread.currentThread().getName()); + ThreadUtil.sleep(3000); + System.out.println("1<---"); + }); + + threadPool.execute(() -> { + System.out.println("2--->" + Thread.currentThread().getName()); + ThreadUtil.sleep(3000); + System.out.println("2<---"); + }); + + threadPool.execute(() -> { + System.out.println("3--->" + Thread.currentThread().getName()); + ThreadUtil.sleep(3000); + System.out.println("3<---"); + }); + + threadPool.execute(() -> { + System.out.println("4--->" + Thread.currentThread().getName()); + ThreadUtil.sleep(3000); + System.out.println("4<---"); + }); + + threadPool.execute(() -> { + System.out.println("5--->" + Thread.currentThread().getName()); + ThreadUtil.sleep(3000); + System.out.println("5<---"); + }); +/* + threadPool.execute(()->{ + System.out.println("6--->"+Thread.currentThread().getName()); + ThreadUtil.sleep(60000); + System.out.println("6<---"); + }); + + threadPool.execute(()->{ + System.out.println("7--->"+Thread.currentThread().getName()); + ThreadUtil.sleep(70000); + System.out.println("7<---"); + });*/ + } catch (Exception e) { + e.printStackTrace(); + } + ThreadUtil.sleep(300000); + } + + private static class DefaultThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public DefaultThreadFactory(String poolName) { + if (StrUtil.isBlank(poolName)) namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; + else namePrefix = poolName + "-"; + } + + public DefaultThreadFactory() { + namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(@NotNull Runnable r) { + String name = namePrefix + threadNumber.getAndIncrement(); + log.debug("创建新线程:{}", name); + Thread t = new Thread(r, name) { + @Override + public void run() { + try { + super.run(); + } finally { + int i = threadNumber.decrementAndGet(); + log.debug("线程结束:{},剩余:{}", this.getName(), i - 1); + } + } + }; + if (t.isDaemon()) t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); + t.setUncaughtExceptionHandler((t1, e) -> log.error("线程异常:{}", t1.getName(), e)); + return t; + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java new file mode 100644 index 0000000..be23b17 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java @@ -0,0 +1,693 @@ +package com.njzscloud.common.core.thread; + +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +@Slf4j +@SuppressWarnings("unchecked") +public class WindowBlockingQueue extends AbstractQueue implements BlockingQueue { + + private final int windowCapacity; + private final int totalCapacity; + private final AtomicInteger totalElementCount = new AtomicInteger(); + private final AtomicInteger windowElementCount = new AtomicInteger(); + private final AtomicInteger standbyElementCount = new AtomicInteger(); + private final ReentrantLock takeLock = new ReentrantLock(); + private final Condition notEmpty = takeLock.newCondition(); + private final ReentrantLock putLock = new ReentrantLock(); + private final Condition notFull = putLock.newCondition(); + private Node head; + private Node border; + private Node last; + + public WindowBlockingQueue() { + this(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public WindowBlockingQueue(int totalCapacity, int windowCapacity) { + if (windowCapacity <= 0 || totalCapacity < windowCapacity) throw new IllegalArgumentException(); + this.totalCapacity = totalCapacity; + this.windowCapacity = windowCapacity; + head = border = last = new Node<>(null); + } + + public WindowBlockingQueue(Collection c) { + this(Integer.MAX_VALUE, Integer.MAX_VALUE); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + int n = 0; + for (E e : c) { + if (e == null) + throw new NullPointerException(); + if (n == windowCapacity) + throw new IllegalStateException("Queue full"); + enqueue(new Node<>(e)); + ++n; + } + windowElementCount.set(n); + } finally { + putLock.unlock(); + } + } + + private void signalNotEmpty() { + takeLock.lock(); + try { + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + } + + private void signalNotFull() { + putLock.lock(); + try { + notFull.signal(); + } finally { + putLock.unlock(); + } + } + + private void enqueue(Node node) { + last = last.next = node; + totalElementCount.incrementAndGet(); + } + + private E dequeue() { + Node h = head; + Node first = h.next; + h.next = h; + head = first; + E x = first.item; + first.item = null; + totalElementCount.decrementAndGet(); + return x; + } + + void fullyLock() { + putLock.lock(); + takeLock.lock(); + } + + void fullyUnlock() { + takeLock.unlock(); + putLock.unlock(); + } + + public int size() { + return windowElementCount.get(); + } + + public int remainingCapacity() { + return windowCapacity - windowElementCount.get(); + } + + public void put(E e) throws InterruptedException { + if (e == null) throw new NullPointerException(); + int c = -1; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.windowElementCount; + putLock.lockInterruptibly(); + try { + while (count.get() == windowCapacity) { + notFull.await(); + } + enqueue(node); + c = count.getAndIncrement(); + if (c + 1 < windowCapacity) + notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) + signalNotEmpty(); + } + + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + if (e == null) throw new NullPointerException(); + long nanos = unit.toNanos(timeout); + int c = -1; + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.windowElementCount; + putLock.lockInterruptibly(); + try { + while (count.get() == windowCapacity) { + if (nanos <= 0) + return false; + nanos = notFull.awaitNanos(nanos); + } + enqueue(new Node(e)); + c = count.getAndIncrement(); + if (c + 1 < windowCapacity) + notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) + signalNotEmpty(); + return true; + } + + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + boolean offered = false; + int c = windowElementCount.get(); + if (c == windowCapacity) return offered; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + if (windowElementCount.get() < windowCapacity) { + enqueue(node); + offered = true; + c = windowElementCount.incrementAndGet(); + border = node; + if (c < windowCapacity) notFull.signal(); + } + } finally { + // log.debug("1、添加元素:总数:{}、窗口区:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + putLock.unlock(); + } + if (c > 0) signalNotEmpty(); + return offered; + } + + public boolean offerStandby(E e) { + if (e == null) throw new NullPointerException(); + boolean offered = false; + int c = totalElementCount.get(); + if (c == totalCapacity) return offered; + Node node = new Node<>(e); + putLock.lock(); + try { + if (totalElementCount.get() < totalCapacity) { + enqueue(node); + offered = true; + c = windowElementCount.get(); + if (c + 1 <= windowCapacity) { + border = node; + windowElementCount.incrementAndGet(); + } else { + standbyElementCount.incrementAndGet(); + } + if (c + 1 < windowCapacity) notFull.signal(); + } + c = windowElementCount.get(); + } finally { + log.debug("3、添加元素:总数:{}、窗口区:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + putLock.unlock(); + } + if (c != 0) signalNotEmpty(); + return offered; + } + + public E take() throws InterruptedException { + E x; + int c = -1; + takeLock.lockInterruptibly(); + try { + while (windowElementCount.get() == 0) { + notEmpty.await(); + } + x = dequeue(); + if (border.next == null) { + c = windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + c = windowElementCount.get(); + } + if (c > 0) notEmpty.signal(); + } finally { + log.debug("1、提取元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + takeLock.unlock(); + } + if (c != windowCapacity) signalNotFull(); + return x; + } + + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + E x = null; + int c = -1; + long nanos = unit.toNanos(timeout); + takeLock.lockInterruptibly(); + try { + while (windowElementCount.get() == 0) { + if (nanos <= 0) return null; + nanos = notEmpty.awaitNanos(nanos); + } + x = dequeue(); + if (border.next == null) { + c = windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + c = windowElementCount.get(); + } + if (c > 0) notEmpty.signal(); + } finally { + // log.debug("2、提取元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + takeLock.unlock(); + } + if (c != windowCapacity) signalNotFull(); + return x; + } + + public E poll() { + if (windowElementCount.get() == 0) return null; + E x = null; + int c = -1; + takeLock.lock(); + try { + if (windowElementCount.get() > 0) { + x = dequeue(); + if (border.next == null) { + c = windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + c = windowElementCount.get(); + } + if (c > 0) notEmpty.signal(); + } + } finally { + log.debug("3、提取元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + takeLock.unlock(); + } + if (c != windowCapacity) signalNotFull(); + return x; + } + + public E peek() { + if (windowElementCount.get() == 0) + return null; + takeLock.lock(); + try { + Node first = head.next; + if (first == null) + return null; + else + return first.item; + } finally { + takeLock.unlock(); + } + } + + void unlink(Node p, Node trail, boolean overBorder) { + totalElementCount.getAndDecrement(); + if (p == border || !overBorder) { + if (border.next == null) { + windowElementCount.getAndDecrement(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + } + } else { + standbyElementCount.decrementAndGet(); + } + p.item = null; + trail.next = p.next; + if (last == p) last = trail; + + if (windowElementCount.get() == windowCapacity) notFull.signal(); + } + + public boolean remove(Object o) { + if (o == null) return false; + fullyLock(); + try { + boolean overBorder = false; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (!overBorder) overBorder = trail == border; + if (o.equals(p.item)) { + unlink(p, trail, overBorder); + return true; + } + } + return false; + } finally { + log.debug("1、删除元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + fullyUnlock(); + } + } + + public boolean contains(Object o) { + if (o == null) return false; + fullyLock(); + try { + for (Node p = head.next; p != null; p = p.next) + if (o.equals(p.item)) + return true; + return false; + } finally { + fullyUnlock(); + } + } + + public Object[] toArray() { + fullyLock(); + try { + int size = windowElementCount.get(); + Object[] a = new Object[size]; + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = p.item; + return a; + } finally { + fullyUnlock(); + } + } + + public T[] toArray(T[] a) { + fullyLock(); + try { + int size = windowElementCount.get(); + if (a.length < size) + a = (T[]) java.lang.reflect.Array.newInstance + (a.getClass().getComponentType(), size); + + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = (T) p.item; + if (a.length > k) + a[k] = null; + return a; + } finally { + fullyUnlock(); + } + } + + public String toString() { + fullyLock(); + try { + Node p = head.next; + if (p == null) + return "[]"; + + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (; ; ) { + E e = p.item; + sb.append(e == this ? "(this Collection)" : e); + p = p.next; + if (p == null) + return sb.append(']').toString(); + sb.append(',').append(' '); + } + } finally { + fullyUnlock(); + } + } + + public void clear() { + fullyLock(); + try { + for (Node p, h = head; (p = h.next) != null; h = p) { + h.next = h; + p.item = null; + } + head = last; + totalElementCount.getAndSet(0); + standbyElementCount.getAndSet(0); + if (windowElementCount.getAndSet(0) == windowCapacity) notFull.signal(); + } finally { + fullyUnlock(); + } + } + + public int drainTo(Collection c) { + return drainTo(c, Integer.MAX_VALUE); + } + + public int drainTo(Collection c, int maxElements) { + if (c == null) + throw new NullPointerException(); + if (c == this) + throw new IllegalArgumentException(); + if (maxElements <= 0) + return 0; + boolean signalNotFull = false; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + int n = Math.min(maxElements, windowElementCount.get()); + + Node h = head; + int i = 0; + try { + while (i < n) { + Node p = h.next; + c.add(p.item); + p.item = null; + h.next = h; + h = p; + ++i; + + if (border.next == null) { + windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + } + } + return n; + } finally { + if (i > 0) { + head = h; + totalElementCount.getAndAdd(-i); + signalNotFull = (windowElementCount.get() == windowCapacity); + } + } + } finally { + takeLock.unlock(); + if (signalNotFull) + signalNotFull(); + } + } + + public Iterator iterator() { + return new Itr(); + } + + public Spliterator spliterator() { + return new LBQSpliterator(this); + } + + static class Node { + E item; + + /** + * One of: + * - the real successor Node + * - this Node, meaning the successor is head.next + * - null, meaning there is no successor (this is the last node) + */ + Node next; + + Node(E x) { + item = x; + } + } + + static final class LBQSpliterator implements Spliterator { + static final int MAX_BATCH = 1 << 25; + final WindowBlockingQueue queue; + Node current; + int batch; + boolean exhausted; + long est; + + LBQSpliterator(WindowBlockingQueue queue) { + this.queue = queue; + this.est = queue.size(); + } + + public long estimateSize() { + return est; + } + + public Spliterator trySplit() { + Node h; + final WindowBlockingQueue q = this.queue; + int b = batch; + int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1; + if (!exhausted && + ((h = current) != null || (h = q.head.next) != null) && + h.next != null) { + Object[] a = new Object[n]; + int i = 0; + Node p = current; + q.fullyLock(); + try { + if (p != null || (p = q.head.next) != null) { + do { + if ((a[i] = p.item) != null) + ++i; + } while ((p = p.next) != null && i < n); + } + } finally { + q.fullyUnlock(); + } + if ((current = p) == null) { + est = 0L; + exhausted = true; + } else if ((est -= i) < 0L) + est = 0L; + if (i > 0) { + batch = i; + return Spliterators.spliterator + (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT); + } + } + return null; + } + + public void forEachRemaining(Consumer action) { + if (action == null) throw new NullPointerException(); + final WindowBlockingQueue q = this.queue; + if (!exhausted) { + exhausted = true; + Node p = current; + do { + E e = null; + q.fullyLock(); + try { + if (p == null) + p = q.head.next; + while (p != null) { + e = p.item; + p = p.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (e != null) + action.accept(e); + } while (p != null); + } + } + + public boolean tryAdvance(Consumer action) { + if (action == null) throw new NullPointerException(); + final WindowBlockingQueue q = this.queue; + if (!exhausted) { + E e = null; + q.fullyLock(); + try { + if (current == null) + current = q.head.next; + while (current != null) { + e = current.item; + current = current.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (current == null) + exhausted = true; + if (e != null) { + action.accept(e); + return true; + } + } + return false; + } + + public int characteristics() { + return Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT; + } + } + + private class Itr implements Iterator { + private Node current; + private Node lastRet; + private E currentElement; + + Itr() { + fullyLock(); + try { + current = head.next; + if (current != null) + currentElement = current.item; + } finally { + fullyUnlock(); + } + } + + public boolean hasNext() { + return current != null; + } + + /** + * Returns the next live successor of p, or null if no such. + *

+ * Unlike other traversal methods, iterators need to handle both: + * - dequeued nodes (p.next == p) + * - (possibly multiple) interior removed nodes (p.item == null) + */ + private Node nextNode(Node p) { + for (; ; ) { + Node s = p.next; + if (s == p) + return head.next; + if (s == null || s.item != null) + return s; + p = s; + } + } + + public E next() { + fullyLock(); + try { + if (current == null) + throw new NoSuchElementException(); + E x = currentElement; + lastRet = current; + current = nextNode(current); + currentElement = (current == null) ? null : current.item; + return x; + } finally { + fullyUnlock(); + } + } + + public void remove() { + if (lastRet == null) + throw new IllegalStateException(); + fullyLock(); + try { + Node node = lastRet; + lastRet = null; + boolean overBorder = false; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (!overBorder) overBorder = trail == border; + if (p == node) { + unlink(p, trail, overBorder); + break; + } + } + } finally { + fullyUnlock(); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java new file mode 100644 index 0000000..5250f71 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java @@ -0,0 +1,137 @@ +package com.njzscloud.common.core.tree; + + +import cn.hutool.core.collection.CollUtil; +import com.njzscloud.common.core.utils.GroupUtil; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 树型结构工具 + */ +public class Tree { + + /** + * 创建树形数据(倒序) + * + * @param src 源集合 + * @param rootId 根节点 ID + * @param 节点 ID 类型 + * @return List<? extends TreeNode<T>> + */ + public static List> listToTreeDesc(List> src, T rootId) { + return listToTree(src.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList()), TreeNode::getId, TreeNode::getPid, TreeNode::setChildren, rootId, true); + } + + /** + * 创建树形数据(正序) + * + * @param src 源集合 + * @param rootId 根节点 ID + * @param 节点 ID 类型 + * @return List<? extends TreeNode<T>> + */ + public static List> listToTreeAsc(List> src, T rootId) { + return listToTree(src.stream().sorted().collect(Collectors.toList()), TreeNode::getId, TreeNode::getPid, TreeNode::setChildren, rootId, false); + } + + /** + * 创建树形数据(排序) + * + * @param src 源集合 + * @param idFn id 提供函数 + * @param pidFn pid 提供函数 + * @param setChildrenFn 子节点设置函数 + * @param rootId 根节点 ID + * @param reverse 是否倒序 + * @param 源集合元素类型 + * @param 节点 ID 类型 + * @return List<M> + */ + public static , T> List listToTree(List src, Function idFn, Function pidFn, BiConsumer> setChildrenFn, T rootId, boolean reverse) { + if (CollUtil.isEmpty(src)) return Collections.emptyList(); + if (reverse) { + src = src + .stream() + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + } + + Map> pid_treeNode_map = GroupUtil.k_a(src, pidFn); + + for (M m : src) { + setChildrenFn.accept(m, pid_treeNode_map.get(idFn.apply(m))); + } + return CollUtil.emptyIfNull(pid_treeNode_map.get(rootId)); + } + + /** + * 创建树形数据(不排序) + * + * @param src 源集合 + * @param idFn id 提供函数 + * @param pidFn pid 提供函数 + * @param setChildrenFn 子节点设置函数 + * @param rootId 根节点 ID + * @param 源集合元素类型 + * @param 节点 ID 类型 + * @return List<M> + */ + public static List listToTree(List src, Function idFn, Function pidFn, BiConsumer> setChildrenFn, T rootId) { + if (CollUtil.isEmpty(src)) return Collections.emptyList(); + + Map> pid_treeNode_map = GroupUtil.k_a(src, pidFn); + + for (M m : src) { + setChildrenFn.accept(m, pid_treeNode_map.get(idFn.apply(m))); + } + return CollUtil.emptyIfNull(pid_treeNode_map.get(rootId)); + } + + /** + * 扁平化树 + * + * @param src 源集合 树形结构 + * @param getChildrenFn 子节点获取函数 + * @param convert 转换函数, 源集合 元素 -> 结果集合 元素 + * @param 源集合 元素类型 + * @param 结果集合 元素类型 + * @return List<D> + */ + public static List treeToList(List src, Function> getChildrenFn, Function convert) { + List dest; + if (CollUtil.isNotEmpty(src)) { + dest = new ArrayList<>(); + treeToList(src, dest, getChildrenFn, convert); + } else { + dest = Collections.emptyList(); + } + return dest; + } + + /** + * 扁平化树 + * + * @param src 源集合 树形结构 + * @param dest 结果集合 + * @param getChildrenFn 子节点获取函数 + * @param convert 转换函数, 源集合 元素 -> 结果集合 元素 + * @param 源集合 元素类型 + * @param 结果集合 元素类型 + */ + public static void treeToList(List src, List dest, Function> getChildrenFn, Function convert) { + if (CollUtil.isNotEmpty(src)) { + for (S s : src) { + D d = convert.apply(s); + dest.add(d); + List children = getChildrenFn.apply(s); + if (CollUtil.isNotEmpty(children)) { + Tree.treeToList(children, dest, getChildrenFn, convert); + } + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java new file mode 100644 index 0000000..18faf02 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.core.tree; + + +import java.util.List; + +/** + * 树结点 + * + * @param Id 的数据类型 + */ +public interface TreeNode extends Comparable> { + /** + * 节点 ID + * + * @return T id + */ + T getId(); + + /** + * 节点 上级 ID + * + * @return T pid + */ + T getPid(); + + /** + * 排序 + * + * @return int + */ + int getSort(); + + /** + * 子节点 + * + * @return List<? extends TreeNode<T>> + */ + List> getChildren(); + + /** + * 设置子节点 + * + * @param children 子节点 + */ + void setChildren(List> children); + + @Override + default int compareTo(TreeNode o) { + return this.getSort() - o.getSort(); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java new file mode 100644 index 0000000..e495e09 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java @@ -0,0 +1,121 @@ +package com.njzscloud.common.core.tuple; + +import java.util.*; + +/** + * 2 元组 + * + * @param <_0> + * @param <_1> + */ +public class Tuple2<_0, _1> implements Iterable { + + /** + * 元组大小 + */ + public final int size = 2; + /** + * 第 0 个数据 + */ + protected final _0 _0_; + /** + * 第 1 个数据 + */ + protected final _1 _1_; + + /** + * 构造方法 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + */ + protected Tuple2(_0 _0_, _1 _1_) { + this._0_ = _0_; + this._1_ = _1_; + } + + /** + * 创建 Tuple2 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @return Tuple2<__0, __1> + */ + public static <__0, __1> Tuple2<__0, __1> create(__0 _0_, __1 _1_) { + return new Tuple2<>(_0_, _1_); + } + + /** + * 获取第 0 个数据 + */ + public _0 get_0() { + return _0_; + } + + /** + * 获取第 1 个数据 + */ + public _1 get_1() { + return _1_; + } + + /** + *

按索引获取数据

+ *

索引越界将返回 null

+ * + * @param index 索引 + * @return T + */ + @SuppressWarnings("unchecked") + public T get(int index) { + switch (index) { + case 0: + return (T) this._0_; + case 1: + return (T) this._1_; + default: + return null; + } + } + + /** + * 获取迭代器 + * + * @return Iterator<Object> + */ + @Override + public Iterator iterator() { + return Collections.unmodifiableList(this.toList()).iterator(); + } + + /** + * 转换成 List + * + * @return List<Object> + */ + public List toList() { + return Arrays.asList(this.toArray()); + } + + /** + * 转换成 数组 + * + * @return Object[] + */ + public Object[] toArray() { + return new Object[]{this._0_, this._1_}; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tuple2 tuple2 = (Tuple2) o; + return Objects.equals(_0_, tuple2._0_) && Objects.equals(_1_, tuple2._1_); + } + + @Override + public int hashCode() { + return Objects.hash(_0_, _1_); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java new file mode 100644 index 0000000..4ab7f51 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java @@ -0,0 +1,84 @@ +package com.njzscloud.common.core.tuple; + +/** + * 3 元组 + * + * @param <_0> + * @param <_1> + * @param <_2> + */ +public class Tuple3<_0, _1, _2> extends Tuple2<_0, _1> { + + /** + * 元组大小 + */ + public final int size = 3; + + /** + * 第 2 个数据 + */ + protected final _2 _2_; + + /** + * 构造方法 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + */ + protected Tuple3(_0 _0_, _1 _1_, _2 _2_) { + super(_0_, _1_); + this._2_ = _2_; + } + + /** + * 创建 Tuple3 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + * @return Tuple3<__0, __1, __2> + */ + public static <__0, __1, __2> Tuple3<__0, __1, __2> create(__0 _0_, __1 _1_, __2 _2_) { + return new Tuple3<>(_0_, _1_, _2_); + } + + /** + * 获取第 2 个数据 + */ + public _2 get_2() { + return _2_; + } + + /** + *

按索引获取数据

+ *

索引越界将返回 null

+ * + * @param index 索引 + * @return T + */ + @Override + @SuppressWarnings("unchecked") + public T get(int index) { + switch (index) { + case 0: + return (T) this._0_; + case 1: + return (T) this._1_; + case 2: + return (T) this._2_; + default: + return null; + } + } + + /** + * 转换成 数组 + * + * @return Object[] + */ + @Override + public Object[] toArray() { + return new Object[]{this._0_, this._1_, this._2_}; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java new file mode 100644 index 0000000..242b4c3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java @@ -0,0 +1,87 @@ +package com.njzscloud.common.core.tuple; + +/** + * 4 元组 + * + * @param <_0> + * @param <_1> + * @param <_2> + * @param <_3> + */ +public class Tuple4<_0, _1, _2, _3> extends Tuple3<_0, _1, _2> { + /** + * 元组大小 + */ + public final int size = 4; + /** + * 第 3 个数据 + */ + protected final _3 _3_; + + /** + * 构造方法 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + * @param _3_ 第 3 个数据 + */ + protected Tuple4(_0 _0_, _1 _1_, _2 _2_, _3 _3_) { + super(_0_, _1_, _2_); + this._3_ = _3_; + } + + /** + * 创建 Tuple4 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + * @param _3_ 第 3 个数据 + * @return Tuple3<__0, __1, __2, __3> + */ + public static <__0, __1, __2, __3> Tuple4<__0, __1, __2, __3> create(__0 _0_, __1 _1_, __2 _2_, __3 _3_) { + return new Tuple4<>(_0_, _1_, _2_, _3_); + } + + /** + * 获取第 3 个数据 + */ + public _3 get_3() { + return _3_; + } + + /** + *

按索引获取数据

+ *

索引越界将返回 null

+ * + * @param index 索引 + * @return T + */ + @Override + @SuppressWarnings("unchecked") + public T get(int index) { + switch (index) { + case 0: + return (T) this._0_; + case 1: + return (T) this._1_; + case 2: + return (T) this._2_; + case 3: + return (T) this._3_; + default: + return null; + } + } + + /** + * 转换成 数组 + * + * @return Object[] + */ + @Override + public Object[] toArray() { + return new Object[]{this._0_, this._1_, this._2_, this._3_}; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java new file mode 100644 index 0000000..b7ca86f --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java @@ -0,0 +1,195 @@ +package com.njzscloud.common.core.utils; + +import java.util.regex.PatternSyntaxException; + +/** + * @see sun.nio.fs.Globs + */ +public class Globs { + private static final String regexMetaChars = ".^$+{[]|()"; + private static final String globMetaChars = "\\*?[{"; + private static final char EOL = 0; // TBD + + private Globs() { + } + + private static boolean isRegexMeta(char c) { + return regexMetaChars.indexOf(c) != -1; + } + + private static boolean isGlobMeta(char c) { + return globMetaChars.indexOf(c) != -1; + } + + private static char next(String glob, int i) { + if (i < glob.length()) { + return glob.charAt(i); + } + return EOL; + } + + /** + * Creates a regex pattern from the given glob expression. + * + * @throws PatternSyntaxException + */ + private static String toRegexPattern(String globPattern, boolean isDos) { + boolean inGroup = false; + StringBuilder regex = new StringBuilder("^"); + + int i = 0; + while (i < globPattern.length()) { + char c = globPattern.charAt(i++); + switch (c) { + case '\\': + // escape special characters + if (i == globPattern.length()) { + throw new PatternSyntaxException("No character to escape", globPattern, i - 1); + } + char next = globPattern.charAt(i++); + if (isGlobMeta(next) || isRegexMeta(next)) { + regex.append('\\'); + } + regex.append(next); + break; + case '/': + if (isDos) { + regex.append("\\\\"); + } else { + regex.append(c); + } + break; + case '[': + // don't match name separator in class + if (isDos) { + regex.append("[[^\\\\]&&["); + } else { + regex.append("[[^/]&&["); + } + if (next(globPattern, i) == '^') { + // escape the regex negation char if it appears + regex.append("\\^"); + i++; + } else { + // negation + if (next(globPattern, i) == '!') { + regex.append('^'); + i++; + } + // hyphen allowed at start + if (next(globPattern, i) == '-') { + regex.append('-'); + i++; + } + } + boolean hasRangeStart = false; + char last = 0; + while (i < globPattern.length()) { + c = globPattern.charAt(i++); + if (c == ']') { + break; + } + if (c == '/' || (isDos && c == '\\')) { + throw new PatternSyntaxException("Explicit 'name separator' in class", + globPattern, i - 1); + } + // TBD: how to specify ']' in a class? + if (c == '\\' || c == '[' || + c == '&' && next(globPattern, i) == '&') { + // escape '\', '[' or "&&" for regex class + regex.append('\\'); + } + regex.append(c); + + if (c == '-') { + if (!hasRangeStart) { + throw new PatternSyntaxException("Invalid range", + globPattern, i - 1); + } + if ((c = next(globPattern, i++)) == EOL || c == ']') { + break; + } + if (c < last) { + throw new PatternSyntaxException("Invalid range", + globPattern, i - 3); + } + regex.append(c); + hasRangeStart = false; + } else { + hasRangeStart = true; + last = c; + } + } + if (c != ']') { + throw new PatternSyntaxException("Missing ']", globPattern, i - 1); + } + regex.append("]]"); + break; + case '{': + if (inGroup) { + throw new PatternSyntaxException("Cannot nest groups", + globPattern, i - 1); + } + regex.append("(?:(?:"); + inGroup = true; + break; + case '}': + if (inGroup) { + regex.append("))"); + inGroup = false; + } else { + regex.append('}'); + } + break; + case ',': + if (inGroup) { + regex.append(")|(?:"); + } else { + regex.append(','); + } + break; + case '*': + if (next(globPattern, i) == '*') { + // crosses directory boundaries + regex.append(".*"); + i++; + } else { + // within directory boundary + if (isDos) { + regex.append("[^\\\\]*"); + } else { + regex.append("[^/]*"); + } + } + break; + case '?': + if (isDos) { + regex.append("[^\\\\]"); + } else { + regex.append("[^/]"); + } + break; + + default: + if (isRegexMeta(c)) { + regex.append('\\'); + } + regex.append(c); + } + } + + if (inGroup) { + throw new PatternSyntaxException("Missing '}", globPattern, i - 1); + } + + return regex.append('$').toString(); + } + + static String toUnixRegexPattern(String globPattern) { + return toRegexPattern(globPattern, false); + } + + static String toWindowsRegexPattern(String globPattern) { + return toRegexPattern(globPattern, true); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java new file mode 100644 index 0000000..d2c8273 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java @@ -0,0 +1,625 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.Jackson; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 对象分组工具 + */ +public class GroupUtil { + + // region K-O + + // region Collection + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 值类型 / 源集合元素类型 + * @param 键类型 + * @return Map<K, SV> + */ + public static Map k_o(Collection src, Function kf) { + return k_o(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, Function kf, Function vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 值类型 / 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, SV> + */ + public static > M k_o(Collection src, Function kf, Supplier mf) { + return k_o(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, Function kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + return k_o(src.stream(), kf, vf, mf); + } + + // endregion + + // region Collection 索引 + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, VT> + */ + public static Map k_o(Collection src, BiFunction kf) { + return k_o(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ * Map 为 HashMap, 键为 kf, 值为 vf

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf K 提供函数 + * @param vf V 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, BiFunction kf, Function vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf K 提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, SV> + */ + public static > M k_o(Collection src, BiFunction kf, Supplier mf) { + return k_o(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf K 提供函数 + * @param vf V 提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, BiFunction kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + M map = mf.get(); + + int i = 0; + for (SV sv : src) { + K k = kf.apply(i, sv); + if (map.containsKey(k)) throw Exceptions.error("重复键:{}", sv); + V v = vf.apply(sv); + map.put(k, v); + i++; + } + return map; + } + + /** + *

分组

+ * Map 为 HashMap, 键为 kf, 值为 vf

+ *

vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, Function kf, BiFunction vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ * Map 为 mf, 键为 kf, 值为 vf

+ *

vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, Function kf, BiFunction vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + M map = mf.get(); + + int i = 0; + for (SV sv : src) { + K k = kf.apply(sv); + if (map.containsKey(k)) throw Exceptions.error("重复键:{}", sv); + V v = vf.apply(i, sv); + map.put(k, v); + i++; + } + return map; + } + + /** + *

分组

+ * Map 为 HashMap, 键为 kf, 值为 vf

+ *

kf vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, BiFunction kf, BiFunction vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ * Map 为 mf, 键为 kf, 值为 vf

+ *

kf vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, BiFunction kf, BiFunction vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + M map = mf.get(); + + int i = 0; + for (SV sv : src) { + K k = kf.apply(i, sv); + if (map.containsKey(k)) throw Exceptions.error("重复键:{}", sv); + V v = vf.apply(i, sv); + map.put(k, v); + i++; + } + return map; + } + // endregion + + // region Stream + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, SV> + */ + public static Map k_o(Stream stream, Function kf) { + return k_o(stream, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Stream stream, Function kf, Function vf) { + return k_o(stream, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, SV> + */ + public static > M k_o(Stream stream, Function kf, Supplier mf) { + return k_o(stream, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Stream stream, Function kf, Function vf, Supplier mf) { + if (stream == null) return MapUtil.empty(null); + BinaryOperator op = (oldVal, val) -> { + throw Exceptions.error("值【{}】与【{}】对应的键重复", Jackson.toJsonStr(oldVal), Jackson.toJsonStr(val)); + }; + return stream.collect(Collectors.toMap(kf, vf, op, mf)); + } + // endregion + + // endregion + + // region K-A + + // region Collection + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Collection src, Function kf) { + return k_a(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Collection src, Function kf, Function vf) { + return k_a(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, List<SV>> + */ + public static >> M k_a(Collection src, Function kf, Supplier mf) { + return k_a(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, List<V>> + */ + public static >> M k_a(Collection src, Function kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + return src.stream().collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toList()))); + } + // endregion + + // region Stream + + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Stream stream, Function kf) { + return k_a(stream, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Stream stream, Function kf, Function vf) { + return k_a(stream, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, List<SV>> + */ + public static >> M k_a(Stream stream, Function kf, Supplier mf) { + return k_a(stream, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, List<V>> + */ + public static >> M k_a(Stream stream, Function kf, Function vf, Supplier mf) { + if (stream == null) return MapUtil.empty(null); + return stream.collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toList()))); + } + // endregion + + // endregion + + // region K-S + // region Collection + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, Set<SV>> + */ + public static Map> k_s(Collection src, Function kf) { + return k_s(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, Set<V>> + */ + public static Map> k_s(Collection src, Function kf, Function vf) { + return k_s(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @param Map 类型 + * @return Map<K, Set<SV>> + */ + public static >> M k_s(Collection src, Function kf, Supplier mf) { + return k_s(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, Set<V>> + */ + public static >> M k_s(Collection src, Function kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + return src.stream().collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toSet()))); + } + // endregion + + // region Stream + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, Set<SV>> + */ + public static Map> k_s(Stream stream, Function kf) { + return k_s(stream, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, Set<V>> + */ + public static Map> k_s(Stream stream, Function kf, Function vf) { + return k_s(stream, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @param Map 类型 + * @return Map<K, Set<SV>> + */ + public static >> M k_s(Stream stream, Function kf, Supplier mf) { + return k_s(stream, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, Set<V>> + */ + public static >> M k_s(Stream stream, Function kf, Function vf, Supplier mf) { + if (stream == null) return MapUtil.empty(null); + return stream.collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toSet()))); + } + // endregion + + // endregion + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java new file mode 100644 index 0000000..2b3d672 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.Exceptions; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** + *

缓存/Redis 等 Key

+ */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class Key { + private final String keyTpl; + + /** + *

创建 KEY

+ *

KEY 字符串模板:可用:{名称} 做为占位符

+ * + * @param keyTpl KEY 字符串模板 + * @return KEY 对象 + */ + public static Key create(String keyTpl) { + Assert.notEmpty(keyTpl, () -> Exceptions.error("KEY 字符串模板不能为空")); + keyTpl = keyTpl.replaceAll("\\{\\w+}", "{}"); + return new Key(keyTpl); + } + + /** + *

填充 KEY 模板

+ * + * @param params 填充参数 + * @return key 值 + */ + public String fill(Object... params) { + int count = StrUtil.count(keyTpl, "{}"); + Assert.isTrue(count == params.length, + () -> Exceptions.error("占位符数量:[{}]与参数数量:[{}]不一致", count, params.length)); + return StrUtil.format(keyTpl, params); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java new file mode 100644 index 0000000..bc2b11d --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java @@ -0,0 +1,243 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.Jackson; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * 响应信息主体 + * + * @param + */ +@Getter +@SuppressWarnings("unchecked") +public final class R { + /** + *

错误码 >= 0

+ *

0 --> 没有错误

+ *

其他 --> 有错误

+ */ + private final int code; + /** + * 是否成功 + */ + private final boolean success; + + /** + * 简略信息 + */ + private final String msg; + /** + * 响应数据 + */ + private T data; + /** + * 详细信息 + */ + private Object message; + + private R(T data, ExceptionMsg msg, Object message) { + this(msg.code, data, msg.msg, message); + } + + @JsonCreator + private R(@JsonProperty("code") int code, + @JsonProperty("data") T data, + @JsonProperty("msg") String msg, + @JsonProperty("message") Object message) { + this.data = data; + this.code = code; + this.msg = msg; + this.message = message; + this.success = code == 0; + } + + /** + * 成功,响应码:0,响应信息:成功,详细信息:null,响应数据:null + * + * @param 响应数据的类型 + * @return R<D> + */ + public static R success() { + return new R<>(0, null, "成功", null); + } + + /** + * 成功,响应码:0,响应信息:成功,详细信息:null + * + * @param data 响应数据 + * @param 响应数据的类型 + * @return R<D> + */ + public static R success(D data) { + return new R<>(0, data, "成功", null); + } + + /** + * 成功,响应码:0,详细信息:null + * + * @param data 响应数据 + * @param msg 响应信息 + * @param 响应数据的类型 + * @return R<D> + */ + public static R success(D data, String msg) { + return new R<>(0, data, msg, null); + } + + /** + * 失败,响应码:11111,响应信息:系统异常!,详细信息:null,响应数据:null + * + * @param 响应数据的类型 + * @return R<D> + */ + public static R failed() { + return new R<>(null, ExceptionMsg.SYS_EXP_MSG, null); + } + + /** + * 失败,详细信息:null,响应数据:null + * + * @param msg 简略错误信息 + * @param 响应数据的类型 + * @return R<D> + * @see ExceptionMsg + */ + public static R failed(ExceptionMsg msg) { + return new R<>(null, msg, null); + } + + public static R failed(ExceptionMsg msg, Object message) { + return new R<>(null, msg, message); + } + + public static R failed(ExceptionMsg msg, String message, Object... param) { + if (StrUtil.isNotBlank(message) && param != null && param.length > 0) { + message = StrUtil.format(message, param); + } + return new R<>(null, msg, message); + } + + /** + * 失败,详细信息:null + * + * @param data 响应数据 + * @param msg 简略错误信息 + * @param 响应数据的类型 + * @return R<D> + * @see ExceptionMsg + */ + public static R failed(D data, ExceptionMsg msg) { + return new R<>(null, msg, null); + } + + public static R failed(D data, ExceptionMsg msg, Object message) { + return new R<>(null, msg, message); + } + + /** + * 设置响应数据 + * + * @param data 响应数据 + * @return R<D> + */ + public R setData(T data) { + this.data = data; + return this; + } + + /** + *

添加响应数据

+ *

需确保 data 为 Map 类型且 Key 为 String,data 为 null 时,会新建

+ * + * @param key 键 + * @param val 值 + * @return R<T> + */ + public R> put(String key, V val) { + if (this.data == null) { + R> r = new R<>(this.code, new HashMap<>(), this.msg, this.message); + r.data.put(key, val); + return r; + } else if (Map.class.isAssignableFrom(data.getClass())) { + ((Map) this.data).put(key, val); + return (R>) this; + } + + throw Exceptions.error("响应信息构建失败"); + } + + /** + *

添加响应数据

+ *

需确保 data 为 List 类型,data 为 null 时,会新建

+ * + * @param val 值 + * @return R<T> + */ + public R> add(V val) { + if (this.data == null) { + R> r = new R<>(this.code, new ArrayList<>(), this.msg, this.message); + r.data.add(val); + return r; + } else if (List.class.isAssignableFrom(data.getClass())) { + ((List) this.data).add(val); + return (R>) this; + } + throw Exceptions.error("响应信息构建失败"); + } + + /** + * 设置详细信息 + * + * @param message 详细信息 + * @return R<T> + */ + public R setMessage(Object message) { + this.message = message; + return this; + } + + /** + * 设置详细信息 + * + * @param message 消息字符串模板,占位符:{} + * @param param 占位符参数 + * @return R<T> + */ + public R setMessage(String message, Object... param) { + if (StrUtil.isNotBlank(message) && param != null && param.length > 0) { + message = StrUtil.format(message, param); + } + this.message = message; + return this; + } + + /** + *

转换

+ *

将响应数据转换为其他对象

+ *

code、msg、message 不变化

+ * + * @param converter 转换器 + * @param 新响应数据的类型 + * @return R<D> + */ + public R convert(Function converter) { + D d = converter.apply(this.data); + return new R<>(this.code, d, this.msg, this.message); + } + + @Override + public String toString() { + return Jackson.toJsonStr(this); + } +} diff --git a/njzscloud-common/njzscloud-common-email/pom.xml b/njzscloud-common/njzscloud-common-email/pom.xml new file mode 100644 index 0000000..0c70ad6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-email/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-email + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + + org.springframework.boot + spring-boot-starter + + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java new file mode 100644 index 0000000..6b187d5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java @@ -0,0 +1,234 @@ +package com.njzscloud.common.email; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.tuple.Tuple2; +import lombok.Getter; + +import javax.mail.util.ByteArrayDataSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 邮件消息 + */ +@Getter +public class MailMessage { + /** + * 收件人邮箱 + */ + private final List tos; + /** + * 邮件主题 + */ + private final String subject; + /** + * 邮件内容 + */ + private final String content; + /** + * 是否为 HTML + */ + private final boolean html; + /** + * 附件列表,0-->附件名称、1-->附件内容 + */ + private final List> attachmentList; + + /** + * 创建邮件消息 + * + * @param tos 收件人邮箱 + * @param subject 邮件主题 + * @param content 邮件内容 + * @param html 是否为 HTML + * @param attachmentList 附件列表,0-->附件名称、1-->附件内容 + */ + private MailMessage(List tos, String subject, String content, boolean html, List> attachmentList) { + this.tos = tos; + this.subject = subject; + this.content = content; + this.html = html; + this.attachmentList = attachmentList; + } + + /** + * 获取构建器 + * + * @return 构建器对象 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * 构建器 + */ + public static class Builder { + /** + * 收件人邮箱 + */ + private List tos; + /** + * 邮件主题 + */ + private String subject; + /** + * 邮件内容 + */ + private String content; + /** + * 是否为 HTML + */ + private boolean html; + /** + * 附件列表,0-->附件名称、1-->附件内容 + */ + private List> attachmentList; + + /** + * 添加收件人邮箱 + * + * @param to 收件人邮箱 + * @return 构建器对象 + */ + public Builder addTo(String to) { + if (this.tos == null) { + this.tos = new ArrayList<>(); + } + this.tos.add(to); + return this; + } + + /** + * 设置收件人邮箱 + * + * @param tos 收件人邮箱 + * @return 构建器对象 + */ + public Builder tos(List tos) { + this.tos = tos; + return this; + } + + /** + * 设置邮件主题 + * + * @param subject 邮件主题 + * @return 构建器对象 + */ + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + /** + * 设置邮件内容 + * + * @param content 邮件内容(常规内容) + * @return 构建器对象 + */ + public Builder content(String content) { + this.content = content; + this.html = false; + return this; + } + + /** + * 设置邮件内容 + * + * @param content 邮件内容(HTML内容) + * @return 构建器对象 + */ + public Builder htmlContent(String content) { + this.content = content; + this.html = true; + return this; + } + + /** + * 设置附件列表 + * + * @param attachmentList 附件列表,0-->附件名称、1-->附件内容 + * @return 构建器对象 + */ + public Builder attachmentList(List> attachmentList) { + this.attachmentList = attachmentList; + return this; + } + + /** + * 添加附件 + * + * @param filename 附件名称 + * @param in 附件 + * @param mime 内容类型 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, InputStream in, String mime) { + Assert.notBlank(filename, "附件名称不能为空"); + Assert.notNull(in, "附件不能为空"); + Assert.notBlank(mime, "内容类型不能为空"); + if (this.attachmentList == null) { + this.attachmentList = new ArrayList<>(); + } + ByteArrayDataSource dataSource; + try (InputStream is = in) { + dataSource = new ByteArrayDataSource(is, mime); + } catch (IOException e) { + throw Exceptions.error(e, "附件读取失败"); + } + this.attachmentList.add(Tuple2.create(filename, dataSource)); + return this; + } + + /** + * 添加附件 + * + * @param filename 附件名称 + * @param bytes 附件 + * @param mime 内容类型 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, byte[] bytes, String mime) { + return addAttachment(filename, new ByteArrayInputStream(bytes), mime); + } + + /** + * 添加附件,内容类型默认:application/octet-stream + * + * @param filename 附件名称 + * @param bytes 附件 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, byte[] bytes) { + return addAttachment(filename, new ByteArrayInputStream(bytes), Mime.BINARY); + } + + /** + * 添加附件,内容类型默认:application/octet-stream + * + * @param filename 附件名称 + * @param in 附件 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, InputStream in) { + return addAttachment(filename, in, Mime.BINARY); + } + + /** + * 构建邮件消息 + * + * @return MailMessage + */ + public MailMessage build() { + Assert.isTrue(this.tos != null && !this.tos.isEmpty(), "收件人邮箱不能为空"); + Assert.notBlank(this.subject, "邮件主题不能为空"); + return new MailMessage(this.tos, this.subject, this.content, this.html, this.attachmentList); + } + } +} diff --git a/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java new file mode 100644 index 0000000..58b618d --- /dev/null +++ b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java @@ -0,0 +1,98 @@ +package com.njzscloud.common.email.util; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.email.MailMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.MailAuthenticationException; +import org.springframework.mail.MailException; +import org.springframework.mail.MailParseException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.mail.util.ByteArrayDataSource; +import java.util.Collections; +import java.util.List; + +/** + * 电子邮件工具 + */ +@Slf4j +public class EMailUtil { + private static final String FROM; + private static final JavaMailSender MAIL_SENDER; + + static { + MAIL_SENDER = SpringUtil.getBean(JavaMailSender.class); + FROM = SpringUtil.getProperty("spring.mail.username"); + } + + /** + * 发送简单文本邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param content 邮件内容 + */ + public void sendSimpleMail(String to, String subject, String content) { + sendSimpleMail(Collections.singletonList(to), subject, content); + } + + /** + * 发送简单文本邮件 + * + * @param tos 接收者邮件 + * @param subject 邮件主题 + * @param content 邮件内容 + */ + public void sendSimpleMail(List tos, String subject, String content) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(FROM); + message.setTo(tos.toArray(new String[0])); + message.setSubject(subject); + message.setText(content); + try { + MAIL_SENDER.send(message); + } catch (MailException e) { + throw Exceptions.error(e, + e instanceof MailParseException ? "邮件消息解析失败" : + e instanceof MailAuthenticationException ? "邮件服务器认证失败" : "邮件发送失败", e); + } + } + + /** + * 发送复杂邮件 + * + * @param mailMessage 消息 + */ + public void sendComplexMail(MailMessage mailMessage) { + MimeMessage message = MAIL_SENDER.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(FROM); + helper.setTo(mailMessage.getTos().toArray(new String[0])); + helper.setSubject(mailMessage.getSubject()); + helper.setText(mailMessage.getContent(), mailMessage.isHtml()); + List> attachmentList = mailMessage.getAttachmentList(); + if (attachmentList != null && !attachmentList.isEmpty()) { + for (Tuple2 dataSource : attachmentList) { + helper.addAttachment(dataSource.get_0(), dataSource.get_1()); + } + } + } catch (MessagingException e) { + throw Exceptions.error(e, "邮件创建失败"); + } + + try { + MAIL_SENDER.send(message); + } catch (MailException e) { + throw Exceptions.error(e, + e instanceof MailParseException ? "邮件消息解析失败" : + e instanceof MailAuthenticationException ? "邮件服务器认证失败" : "邮件发送失败"); + } + } +} diff --git a/njzscloud-common/njzscloud-common-gen/pom.xml b/njzscloud-common/njzscloud-common-gen/pom.xml new file mode 100644 index 0000000..0007809 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-gen + jar + + njzscloud-common-gen + http://maven.apache.org + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + com.njzscloud + njzscloud-common-mvc + provided + + + + com.njzscloud + njzscloud-common-mp + provided + + + + com.ibeetl + beetl + 3.19.2.RELEASE + + + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplController.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplController.java new file mode 100644 index 0000000..0ab1f26 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplController.java @@ -0,0 +1,94 @@ +package com.njzscloud.common.gen; + +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.gen.support.Generator; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * 代码模板 + */ +@Slf4j +@RestController +@RequestMapping("/sys_tpl") +@RequiredArgsConstructor +public class SysTplController { + + private final SysTplService sysTplService; + + /** + * 新增 + * + * @param sysTplEntity 数据 + */ + @PostMapping("/add") + public R add(@RequestBody SysTplEntity sysTplEntity) { + sysTplService.add(sysTplEntity); + return R.success(); + } + + /** + * 修改 + * + * @param sysTplEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody SysTplEntity sysTplEntity) { + sysTplService.modify(sysTplEntity); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysTplService.del(ids); + return R.success(); + } + + @PostMapping("/generate") + public void generate(@RequestParam String tplName, @RequestBody(required = false) Map data, HttpServletResponse response) { + try { + response.setContentType(Mime.u8Val(Mime.TXT)); + Generator.generate(tplName, data, response.getOutputStream()); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + /** + * 详情 + * + * @param id Id + * @return SysTplEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysTplService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysTplEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysTplEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysTplEntity sysTplEntity) { + return R.success(sysTplService.paging(pageParam, sysTplEntity)); + } + +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplEntity.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplEntity.java new file mode 100644 index 0000000..6638c4b --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplEntity.java @@ -0,0 +1,55 @@ +package com.njzscloud.common.gen; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njzscloud.common.gen.contant.TplCategory; +import com.njzscloud.common.gen.support.Tpl; +import com.njzscloud.common.mp.support.handler.j.JsonTypeHandler; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Map; + +/** + * 代码模板 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName(value = "sys_tpl", autoResultMap = true) +public class SysTplEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 模板名称 + */ + private String tplName; + + /** + * 模板类型; 字典编码:tpl_category + */ + private TplCategory tplCategory; + + /** + * 模板内容 + */ + @TableField(typeHandler = JsonTypeHandler.class) + private Tpl tpl; + + /** + * 模型数据 + */ + @TableField(typeHandler = JsonTypeHandler.class) + private Map modelData; + +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplMapper.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplMapper.java new file mode 100644 index 0000000..a52049c --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.gen; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 代码模板 + */ +@Mapper +public interface SysTplMapper extends BaseMapper { + +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplService.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplService.java new file mode 100644 index 0000000..e30d03f --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/SysTplService.java @@ -0,0 +1,87 @@ +package com.njzscloud.common.gen; + +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.gen.support.TemplateEngine; +import com.njzscloud.common.gen.support.Tpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +/** + * 代码模板 + */ +@Slf4j +@Service +public class SysTplService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysTplEntity 数据 + */ + public void add(SysTplEntity sysTplEntity) { + Tpl tpl = sysTplEntity.getTpl(); + if (tpl == null) { + sysTplEntity.setTpl(new Tpl()); + } + + Map modelData = sysTplEntity.getModelData(); + if (modelData == null) { + sysTplEntity.setModelData(MapUtil.empty()); + } + this.save(sysTplEntity); + } + + /** + * 修改 + * + * @param sysTplEntity 数据 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(SysTplEntity sysTplEntity) { + Long id = sysTplEntity.getId(); + SysTplEntity oldData = this.getById(id); + TemplateEngine.rmEngine(oldData.getTplName()); + this.updateById(sysTplEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysTplEntity 结果 + */ + public SysTplEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysTplEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysTplEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, SysTplEntity sysTplEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysTplEntity))); + } + +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java new file mode 100644 index 0000000..d9544e3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.gen.config; + +import com.njzscloud.common.gen.support.DbMetaData; +import com.njzscloud.common.gen.SysTplController; +import com.njzscloud.common.gen.SysTplService; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +@Configuration +@MapperScan("com.njzscloud.common.gen") +public class GenAutoConfiguration { + + @Bean + public SysTplService sysTplService() { + return new SysTplService(); + } + + @Bean + public SysTplController sysTplController(SysTplService sysTplService) { + return new SysTplController(sysTplService); + } + + @Bean + public DbMetaData dbMetaData(DataSource dataSource) { + return new DbMetaData(dataSource); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java new file mode 100644 index 0000000..45b4b7c --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.gen.contant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import com.njzscloud.common.core.ienum.DictStr; + +/** + * 字典代码:tpl_category + * 字典名称:模板类型 + */ +@Getter +@RequiredArgsConstructor +public enum TplCategory implements DictStr { + Controller("Controller", "Controller"), + Service("Service", "Service"), + Mapper("Mapper", "Mapper"), + MapperXml("MapperXml", "MapperXml"), + Entity("Entity", "Entity"), + ; + private final String val; + private final String txt; +} + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java new file mode 100644 index 0000000..5725f62 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java @@ -0,0 +1,40 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import org.beetl.core.Context; +import org.beetl.core.Function; + +import java.util.Map; + +public final class Btl { + public static final Function toCamelCase = (Object[] paras, Context ctx) -> StrUtil.toCamelCase(paras[0].toString()); + public static final Function upperFirst = (Object[] paras, Context ctx) -> StrUtil.upperFirst(paras[0].toString()); + public static final Function isBlank = (Object[] paras, Context ctx) -> StrUtil.isBlank(paras[0].toString()); + public static final Function subAfter = (Object[] paras, Context ctx) -> StrUtil.subAfter(paras[0].toString(), paras[1].toString(), false); + public static final Function sqlDataTypeMap = new Function() { + private final Map> map = MapUtil.>builder() + .put("DEFAULT", MapUtil.builder().put("importStatement", "").put("dataType", "").build()) + .put("VARCHAR", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("CHAR", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("LONGTEXT", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("TEXT", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("BIT", MapUtil.builder().put("importStatement", "").put("dataType", "Boolean").build()) + .put("TINYINT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("SMALLINT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("MEDIUMINT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("INT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("BIGINT", MapUtil.builder().put("importStatement", "").put("dataType", "Long").build()) + .put("DOUBLE", MapUtil.builder().put("importStatement", "").put("dataType", "DOUBLE").build()) + .put("DECIMAL", MapUtil.builder().put("importStatement", "import java.math.BigDecimal;").put("dataType", "BigDecimal").build()) + .put("DATE", MapUtil.builder().put("importStatement", "import java.time.LocalDate;").put("dataType", "LocalDate").build()) + .put("TIME", MapUtil.builder().put("importStatement", "import java.time.LocalTime;").put("dataType", "LocalTime").build()) + .put("DATETIME", MapUtil.builder().put("importStatement", "import java.time.LocalDateTime;").put("dataType", "LocalDateTime").build()) + .build(); + + @Override + public Object call(Object[] paras, Context ctx) { + return map.getOrDefault(paras[0].toString().toUpperCase(), map.get("DEFAULT")); + } + }; +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java new file mode 100644 index 0000000..4e8cf26 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java @@ -0,0 +1,73 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +public class DbMetaData { + private final DataSource dataSource; + + public List> getMetaData(String tableName) { + try (Connection connection = dataSource.getConnection()) { + return getTables(connection, tableName); + } catch (Exception e) { + log.error("获取数据库元数据失败", e); + return Collections.emptyList(); + } + } + + private List> getTableColumns(Connection conn, String tableName) throws Exception { + List> columns = new ArrayList<>(); + + DatabaseMetaData metaData = conn.getMetaData(); + try (ResultSet columnsData = metaData.getColumns(null, null, tableName, "%")) { + while (columnsData.next()) { + columns.add(MapUtil.builder() + .put("name", columnsData.getString("COLUMN_NAME")) + .put("dataType", columnsData.getString("TYPE_NAME")) + .put("size", columnsData.getInt("COLUMN_SIZE")) + .put("nullable", columnsData.getInt("NULLABLE") == DatabaseMetaData.columnNullable) + .put("defaultValue", columnsData.getString("COLUMN_DEF")) + .put("comment", columnsData.getString("REMARKS")) + .build() + ); + } + } + try (ResultSet primaryKeyData = metaData.getPrimaryKeys(null, null, tableName)) { + while (primaryKeyData.next()) { + String primaryKeyColumn = primaryKeyData.getString("COLUMN_NAME"); + columns.forEach(column -> column.put("primaryKey", column.get("name").equals(primaryKeyColumn))); + } + } + return columns; + } + + private List> getTables(Connection conn, String tableName) throws Exception { + List> tableInfos = new ArrayList<>(); + DatabaseMetaData metaData = conn.getMetaData(); + try (ResultSet tablesData = metaData.getTables(null, null, StrUtil.isNotBlank(tableName) ? tableName : "%", new String[]{"TABLE"})) { + while (tablesData.next()) { + String tableName_ = tablesData.getString("TABLE_NAME"); + List> columns = getTableColumns(conn, tableName_); + tableInfos.add(MapUtil.builder() + .put("name", tableName_) + .put("comment", tablesData.getString("REMARKS")) + .put("columns", columns) + .build()); + } + } + return tableInfos; + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java new file mode 100644 index 0000000..6d82af8 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java @@ -0,0 +1,55 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.gen.SysTplEntity; +import com.njzscloud.common.gen.SysTplService; +import lombok.extern.slf4j.Slf4j; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class Generator { + private static Tuple3 generate0(String tplName, Map data) { + DbMetaData dbMetaData = SpringUtil.getBean(DbMetaData.class); + SysTplService sysTplService = SpringUtil.getBean(SysTplService.class); + List> table = dbMetaData.getMetaData(data.getOrDefault("tableName", "%").toString()); + SysTplEntity tplEntity = sysTplService.getOne(Wrappers.lambdaQuery() + .eq(SysTplEntity::getTplName, tplName)); + + Tpl tpl = tplEntity.getTpl(); + Map modelData = tplEntity.getModelData(); + HashMap map = new HashMap<>(modelData); + map.put("table", CollUtil.isNotEmpty(table) ? table.get(0) : MapUtil.empty()); + map.putAll(data); + log.info("数据:{}", Jackson.toJsonStr(map)); + TemplateEngine templateEngine = TemplateEngine.getEngine(tplName, tpl); + String content = templateEngine.renderContent(map); + String dir = templateEngine.renderDir(map); + String filename = templateEngine.renderFilename(map); + return Tuple3.create(dir, filename, content); + } + + public static void generate(String tplName, Map data) { + Tuple3 tuple3 = generate0(tplName, data == null ? MapUtil.empty() : data); + String dir = tuple3.get_0(); + FileUtil.mkdir(dir); + FileUtil.writeUtf8String(tuple3.get_2(), dir + "/" + tuple3.get_1()); + } + + public static void generate(String tplName, Map data, OutputStream out) { + Tuple3 tuple3 = generate0(tplName, data == null ? MapUtil.empty() : data); + log.info("结果:{}", tuple3.get_2()); + IoUtil.write(out, false, tuple3.get_2().getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java new file mode 100644 index 0000000..02ea757 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java @@ -0,0 +1,75 @@ +package com.njzscloud.common.gen.support; + +import com.njzscloud.common.core.jackson.Jackson; +import lombok.extern.slf4j.Slf4j; +import org.beetl.core.Configuration; +import org.beetl.core.GroupTemplate; +import org.beetl.core.Template; +import org.beetl.core.resource.MapResourceLoader; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class TemplateEngine { + + private static final GroupTemplate GT; + private static final MapResourceLoader LOADER = new MapResourceLoader(); + private static final Map ENGINE = new ConcurrentHashMap<>(); + + static { + GroupTemplate getTemplate_; + try { + Configuration cfg = Configuration.defaultConfiguration(); + getTemplate_ = new GroupTemplate(LOADER, cfg); + getTemplate_.registerFunction("toCamelCase", Btl.toCamelCase); + getTemplate_.registerFunction("upperFirst", Btl.upperFirst); + getTemplate_.registerFunction("isBlank", Btl.isBlank); + getTemplate_.registerFunction("subAfter", Btl.subAfter); + getTemplate_.registerFunction("sqlDataTypeMap", Btl.sqlDataTypeMap); + } catch (IOException e) { + getTemplate_ = null; + log.error("模板引擎初始化失败", e); + } + GT = getTemplate_; + } + + private final String tplName; + + private TemplateEngine(String tplName, Tpl tpl) { + this.tplName = tplName; + LOADER.put(tplName + "-content", tpl.getContent()); + LOADER.put(tplName + "-dir", tpl.getDir()); + LOADER.put(tplName + "-filename", tpl.getFilename()); + } + + public static TemplateEngine getEngine(String tplName, Tpl tpl) { + return ENGINE.computeIfAbsent(tplName, key -> new TemplateEngine(tplName, tpl)); + } + + public static void rmEngine(String tplName) { + ENGINE.remove(tplName); + LOADER.remove(tplName + "-content"); + LOADER.remove(tplName + "-dir"); + LOADER.remove(tplName + "-filename"); + } + + public String render(String tplName, Map data) { + Template template = GT.getTemplate(tplName); + template.binding(data); + return template.render(); + } + + public String renderContent(Map data) { + return render(tplName + "-content", data); + } + + public String renderDir(Map data) { + return render(tplName + "-dir", data); + } + + public String renderFilename(Map data) { + return render(tplName + "-filename", data); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java new file mode 100644 index 0000000..e507357 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.gen.support; + +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Setter +@ToString +@Accessors(chain = true) +public class Tpl { + private String content; + private String dir; + private String filename; + + public String getContent() { + return content == null ? "" : content; + } + + public String getDir() { + return dir == null ? "" : dir; + } + + public String getFilename() { + return filename == null ? "" : filename; + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..fb215b7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.gen.config.GenAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/controller.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/controller.btl new file mode 100644 index 0000000..8855e1d --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/controller.btl @@ -0,0 +1,74 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = table.name; +%> +package ${basePackage}.controller; + +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import ${basePackage}.entity.${entityClass}; +import ${basePackage}.service.${serviceClass}; + +/** + * ${table.comment} + */ +@Slf4j +@RestController +@RequestMapping("/${baseUrl}") +@RequiredArgsConstructor +public class ${controllerClass} { + private final ${serviceClass} ${serviceInstance}; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody ${entityClass} ${entityInstance}) { + ${serviceInstance}.add(${entityInstance}); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody ${entityClass} ${entityInstance}) { + ${serviceInstance}.modify(${entityInstance}); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + ${serviceInstance}.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R<${entityClass}> detail(@RequestParam Long id) { + return R.success(${serviceInstance}.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, ${entityClass} ${entityInstance}) { + return R.success(${serviceInstance}.paging(pageParam, ${entityInstance})); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/entity.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/entity.btl new file mode 100644 index 0000000..3defff6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/entity.btl @@ -0,0 +1,56 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = table.name; +%> +package ${basePackage}.entity; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; +<%for(column in table.columns) { + var map = sqlDataTypeMap(column.dataType); +%> +<%if(!isBlank(map.importStatement)){%> +${map.importStatement} +<%}%> +<%}%> + +/** + * ${table.comment} + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("${table.name}") +public class ${entityClass} { + +<% +for(column in table.columns){ + var map = sqlDataTypeMap(column.dataType); +%> + /** + * ${column.comment} + */ + <%if(column.primaryKey){%> + @TableId(type = IdType.ASSIGN_ID) + <%}%> + <%if(column.name == "creator_id" || column.name == "create_time"){%> + @TableId(type = IdType.ASSIGN_ID) + <%}else if(column.name == "modifier_id" || column.name == "modify_time"){%> + @TableField(fill = FieldFill.INSERT_UPDATE) + <%}else if(column.name == "deleted"){%> + @TableLogic + <%}%> + private ${map.dataType} ${toCamelCase(column.name)}; + +<%}%> +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper.btl new file mode 100644 index 0000000..2e28cdc --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper.btl @@ -0,0 +1,22 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = table.name; +%> +package ${basePackage}.mapper; + +import org.apache.ibatis.annotations.Mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import ${basePackage}.entity.${entityClass}; + +/** + * ${table.comment} + */ +@Mapper +public interface ${mapperClass} extends BaseMapper<${entityClass}> { +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper_xml.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper_xml.btl new file mode 100644 index 0000000..2590e5c --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/mapper_xml.btl @@ -0,0 +1,14 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = table.name; +%> + + + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/service.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/service.btl new file mode 100644 index 0000000..2572aa5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/service.btl @@ -0,0 +1,69 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = table.name; +%> +package ${basePackage}.service; + +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import ${basePackage}.entity.${entityClass}; +import ${basePackage}.mapper.${mapperClass}; + +/** + * ${table.comment} + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ${serviceClass} extends ServiceImpl<${mapperClass}, ${entityClass}> implements IService<${entityClass}> { + + /** + * 新增 + */ + public void add(${entityClass} ${entityInstance}) { + this.save(${entityInstance}); + } + + /** + * 修改 + */ + public void modify(${entityClass} ${entityInstance}) { + this.updateById(${entityInstance}); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + */ + public ${entityClass} detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + */ + public PageResult<${entityClass}> paging(PageParam pageParam, ${entityClass} ${entityInstance}) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.<${entityClass}>query(${entityInstance}))); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json new file mode 100644 index 0000000..6cb9982 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json @@ -0,0 +1,10 @@ +{ + "tplName": "Service", + "tplCategory": "Service", + "tpl": { + "dir": "", + "filename": "", + "content": "<%\nvar entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix));\nvar controllerClass = upperFirst(entityName) + \"Controller\";\nvar entityClass = upperFirst(entityName) + \"Entity\";\nvar entityInstance = entityName + \"Entity\";\nvar serviceClass = upperFirst(entityName) + \"Service\";\nvar serviceInstance = entityName + \"Service\";\nvar mapperClass = upperFirst(entityName) + \"Mapper\";\nvar baseUrl = entityName + \"Service\";\n%>\npackage ${basePackage}.service;\n\nimport lombok.extern.slf4j.Slf4j;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Service;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.baomidou.mybatisplus.core.toolkit.Wrappers;\nimport com.njzscloud.common.mp.support.PageParam;\nimport com.njzscloud.common.mp.support.PageResult;\nimport java.util.List;\nimport org.springframework.transaction.annotation.Transactional;\nimport ${basePackage}.entity.${entityClass};\nimport ${basePackage}.mapper.${mapperClass};\n\n/**\n * ${table.comment}\n */\n@Slf4j\n@Service\n@RequiredArgsConstructor\npublic class ${serviceClass} extends ServiceImpl<${mapperClass}, ${entityClass}> implements IService<${entityClass}> {\n\n /**\n * 新增\n */\n public void add(${entityClass} ${entityInstance}) {\n this.save(${entityInstance});\n }\n\n /**\n * 修改\n */\n public void modify(${entityClass} ${entityInstance}) {\n this.updateById(${entityInstance});\n }\n\n /**\n * 删除\n */\n @Transactional(rollbackFor = Exception.class)\n public void del(List ids) {\n this.removeBatchByIds(ids);\n }\n\n /**\n * 详情\n */\n public ${entityClass} detail(Long id) {\n return this.getById(id);\n }\n\n /**\n * 分页查询\n */\n public PageResult<${entityClass}> paging(PageParam pageParam, ${entityClass} ${entityInstance}) {\n return PageResult.of(this.page(pageParam.toPage(), Wrappers.<${entityClass}>query(${entityInstance})));\n }\n}\n" + }, + "modelData": {} +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl_update.json b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl_update.json new file mode 100644 index 0000000..4957a26 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl_update.json @@ -0,0 +1,8 @@ +{ + "id": "1947531036271337474", + "tpl": { + "dir": "", + "filename": "", + "content": "<%\nvar entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix));\nvar controllerClass = upperFirst(entityName) + \"Controller\";\nvar entityClass = upperFirst(entityName) + \"Entity\";\nvar entityInstance = entityName + \"Entity\";\nvar serviceClass = upperFirst(entityName) + \"Service\";\nvar serviceInstance = entityName + \"Service\";\nvar baseUrl = entityName + \"Service\";\n%>\npackage ${basePackage}.entity;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.experimental.Accessors;\nimport com.baomidou.mybatisplus.annotation.*;\nimport lombok.ToString;\n<%for(column in table.columns) {\n var map = sqlDataTypeMap(column.dataType);\n%>\n<%if(!isBlank(map.importStatement)){%>\n${map.importStatement}\n<%}%>\n<%}%>\n\n/**\n * ${table.comment}\n */\n@Getter\n@Setter\n@ToString\n@Accessors(chain = true)\n@TableName(\"${table.name}\")\npublic class ${entityClass} {\n\n<%\nfor(column in table.columns){\n var map = sqlDataTypeMap(column.dataType);\n%>\n /**\n * ${column.comment}\n */\n <%if(column.primaryKey){%>\n @TableId(type = IdType.ASSIGN_ID)\n <%}%>\n <%if(column.name == \"creator_id\" || column.name == \"create_time\"){%>\n @TableId(type = IdType.ASSIGN_ID)\n <%}else if(column.name == \"modifier_id\" || column.name == \"modify_time\"){%>\n @TableField(fill = FieldFill.INSERT_UPDATE)\n <%}else if(column.name == \"deleted\"){%>\n @TableLogic\n <%}%>\n private ${map.dataType} ${toCamelCase(column.name)};\n\n<%}%>\n}\n" + } +} diff --git a/njzscloud-common/njzscloud-common-job/pom.xml b/njzscloud-common/njzscloud-common-job/pom.xml new file mode 100644 index 0000000..0b0e0ba --- /dev/null +++ b/njzscloud-common/njzscloud-common-job/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-job + + + 8 + 8 + UTF-8 + 2.4.1 + + + + com.njzscloud + njzscloud-common-core + provided + + + + + com.xuxueli + xxl-job-core + + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.springframework.boot + spring-boot-starter + + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + diff --git a/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlAdminProperties.java b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlAdminProperties.java new file mode 100644 index 0000000..4a9ae17 --- /dev/null +++ b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlAdminProperties.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.job; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class XxlAdminProperties { + /** + * 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。 + * 执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; + */ + private String protocol = "http"; + private String ip; + private String port; + private String contextPath = "/xxl-job-admin"; + + /** + * 执行器通讯TOKEN [选填]:非空时启用; + */ + private String accessToken = "XXLJob"; + + public String getAddresses() { + return protocol + "://" + ip + ":" + port + contextPath; + } +} diff --git a/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlExecutorProperties.java b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlExecutorProperties.java new file mode 100644 index 0000000..1c5d31d --- /dev/null +++ b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlExecutorProperties.java @@ -0,0 +1,34 @@ +package com.njzscloud.common.job; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class XxlExecutorProperties { + /** + * 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册 + */ + private String appName; + + /** + * 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP + * ,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务" + */ + private String ip; + + /** + * 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; + */ + private Integer port = 0; + + /** + * 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; + */ + private String logPath = "logs/xxl-job"; + + /** + * 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效; + */ + private Integer logRetentionDays = 30; +} diff --git a/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobAutoConfiguration.java b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobAutoConfiguration.java new file mode 100644 index 0000000..9210b2b --- /dev/null +++ b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobAutoConfiguration.java @@ -0,0 +1,28 @@ +package com.njzscloud.common.job; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({XxlJobProperties.class}) +@ConditionalOnProperty(value = "xxl-job.enable", havingValue = "true") +public class XxlJobAutoConfiguration { + + @Bean + public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobProperties xxlJobProperties) { + XxlAdminProperties xxlAdminProperties = xxlJobProperties.getAdmin(); + XxlExecutorProperties xxlExecutorProperties = xxlJobProperties.getExecutor(); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(xxlAdminProperties.getAddresses()); + xxlJobSpringExecutor.setAppname(xxlExecutorProperties.getAppName()); + xxlJobSpringExecutor.setIp(xxlExecutorProperties.getIp()); + xxlJobSpringExecutor.setPort(xxlExecutorProperties.getPort()); + xxlJobSpringExecutor.setAccessToken(xxlAdminProperties.getAccessToken()); + xxlJobSpringExecutor.setLogPath(xxlExecutorProperties.getLogPath()); + xxlJobSpringExecutor.setLogRetentionDays(xxlExecutorProperties.getLogRetentionDays()); + return xxlJobSpringExecutor; + } +} diff --git a/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobProperties.java b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobProperties.java new file mode 100644 index 0000000..52ed1a1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-job/src/main/java/com/njzscloud/common/job/XxlJobProperties.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.job; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "xxl-job") +public class XxlJobProperties { + private boolean enable = false; + private XxlAdminProperties admin = new XxlAdminProperties(); + + private XxlExecutorProperties executor = new XxlExecutorProperties(); +} diff --git a/njzscloud-common/njzscloud-common-job/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-job/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d497d6e --- /dev/null +++ b/njzscloud-common/njzscloud-common-job/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.job.XxlJobAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-minio/pom.xml b/njzscloud-common/njzscloud-common-minio/pom.xml new file mode 100644 index 0000000..fccb22d --- /dev/null +++ b/njzscloud-common/njzscloud-common-minio/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-minio + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + io.minio + minio + 8.5.17 + + + + diff --git a/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioAutoConfiguration.java b/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioAutoConfiguration.java new file mode 100644 index 0000000..2de5499 --- /dev/null +++ b/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioAutoConfiguration.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.minio.config; + +import io.minio.MinioAsyncClient; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(MinioProperties.class) +public class MinioAutoConfiguration { + + @Bean(destroyMethod = "close") + public MinioAsyncClient minioClient(MinioProperties minioProperties) { + String endpoint = minioProperties.getEndpoint(); + String accessKey = minioProperties.getAccessKey(); + String secretKey = minioProperties.getSecretKey(); + return MinioAsyncClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + } +} diff --git a/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioProperties.java b/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioProperties.java new file mode 100644 index 0000000..da05dc2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/config/MinioProperties.java @@ -0,0 +1,18 @@ +package com.njzscloud.common.minio.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@Getter +@Setter +@ConfigurationProperties("oss.minio") +public class MinioProperties { + private String endpoint; + private String accessKey; + private String secretKey; + private String bucketName; +} diff --git a/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/util/Minio.java b/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/util/Minio.java new file mode 100644 index 0000000..24e1375 --- /dev/null +++ b/njzscloud-common/njzscloud-common-minio/src/main/java/com/njzscloud/common/minio/util/Minio.java @@ -0,0 +1,157 @@ +package com.njzscloud.common.minio.util; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.google.common.collect.HashMultimap; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import io.minio.*; +import io.minio.errors.*; +import io.minio.http.Method; +import io.minio.messages.ListPartsResult; +import io.minio.messages.Part; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class Minio { + private static final MinioAsyncClient CLIENT; + private static final Map, String> ERROR_MAP = new HashMap<>(); + private static final String BUCKET_NAME; + + static { + CLIENT = SpringUtil.getBean(MinioAsyncClient.class); + ERROR_MAP.put(ErrorResponseException.class, "文件存储服务响应失败"); + ERROR_MAP.put(InsufficientDataException.class, "文件读取失败"); + ERROR_MAP.put(InternalException.class, "文件服务器错误"); + ERROR_MAP.put(InvalidKeyException.class, "缺少 HMAC SHA-256 依赖"); + ERROR_MAP.put(InvalidResponseException.class, "服务器未响应"); + ERROR_MAP.put(IOException.class, "文件读取失败"); + ERROR_MAP.put(NoSuchAlgorithmException.class, "缺少 MD5 或 SHA-256 依赖"); + ERROR_MAP.put(XmlParserException.class, "数据解析失败"); + BUCKET_NAME = SpringUtil.getBean(com.njzscloud.common.minio.config.MinioProperties.class).getBucketName(); + if (StrUtil.isNotBlank(BUCKET_NAME)) { + createBucket(BUCKET_NAME); + } + } + + public static void createBucket(String bucketName) { + Assert.notBlank(bucketName, "未指明存储位置"); + boolean exists = Minio.execSync(() -> CLIENT.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())); + if (exists) return; + Minio.execSync(() -> CLIENT.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build())); + } + + public static Tuple2> startMltUpload(String objectName, String contentType, int partNum) { + HashMultimap header = HashMultimap.create(); + header.put("Content-Type", contentType); + return Minio.execSync(() -> CLIENT + .createMultipartUploadAsync(BUCKET_NAME, null, objectName, header, null) + .thenApply(it -> { + String uploadId = it.result().uploadId(); + LinkedList urls = new LinkedList<>(); + for (int i = 0; i < partNum; i++) { + int partNumber = i + 1; + String url = obtainPresignedUrl(objectName + "." + partNumber, MapUtil.builder() + .put("partNumber", Integer.toString(partNumber)) + .put("uploadId", uploadId) + .build()); + urls.add(url); + } + return Tuple2.create(uploadId, urls); + })); + } + + public static void endMltUpload(String objectName, String uploadId, int partNum) { + CompletableFuture.runAsync(() -> { + for (int i = 0; i < partNum % 1000 + 1; i++) { + int partNumberMarker = i + 1; + execSync(() -> CLIENT.listPartsAsync(BUCKET_NAME, + null, + objectName, + null, + partNumberMarker, + uploadId, + null, null) + .thenAccept(it -> { + ListPartsResult result = it.result(); + List parts = result.partList(); + execSync(() -> CLIENT.completeMultipartUploadAsync(BUCKET_NAME, + null, + objectName, + uploadId, + parts.toArray(new Part[0]), + null, + null + )); + }) + ); + } + }); + } + + public static String obtainPresignedUrl(String objectName, Map extraQueryParams) { + return exec(() -> CLIENT.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() + .method(Method.PUT) + .bucket(BUCKET_NAME) + .object(objectName) + .expiry(1, TimeUnit.DAYS) + .extraQueryParams(extraQueryParams) + .build())); + } + + public static Map obtainPresignedUrl(String bucketName, String objectName) { + return exec(() -> { + PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1)); + policy.addEqualsCondition("key", objectName); + Map data = CLIENT.getPresignedPostFormData(policy); + data.put("key", objectName); + data.put("bucketName", bucketName); + data.put("objectName", objectName); + return data; + }); + } + + public static Tuple2 obtainFile(String bucketName, String objectName) { + GetObjectResponse response = execSync(() -> CLIENT.getObject(GetObjectArgs.builder() + .object(objectName) + .bucket(bucketName).build())); + return Tuple2.create(response, response.headers().get("Content-Type")); + } + + private static T execSync(MinioExec> fn) { + try { + return fn.exec().get(); + } catch (Exception e) { + throw Exceptions.error(e, ERROR_MAP.getOrDefault(e.getClass(), "文件服务器错误")); + } + } + + private static T exec(MinioExec fn) { + try { + return fn.exec(); + } catch (Exception e) { + throw Exceptions.error(e, ERROR_MAP.getOrDefault(e.getClass(), "文件服务器错误")); + } + } + + public interface MinioExec { + T exec() throws Exception; + } +} diff --git a/njzscloud-common/njzscloud-common-minio/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-minio/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..bca7e99 --- /dev/null +++ b/njzscloud-common/njzscloud-common-minio/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.minio.config.MinioAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-mp/pom.xml b/njzscloud-common/njzscloud-common-mp/pom.xml new file mode 100644 index 0000000..8e023c5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-mp + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-security + provided + + + com.jcraft + jsch + 0.1.55 + + + + com.mysql + mysql-connector-j + + + + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + mybatis-plus-jsqlparser-4.9 + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DBTunnelAutoConfiguration.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DBTunnelAutoConfiguration.java new file mode 100644 index 0000000..bf11311 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DBTunnelAutoConfiguration.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.mp.config; + +import com.njzscloud.common.mp.support.DBTunnel; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(prefix = "mybatis-plus.tunnel", name = "enable", havingValue = "true") +@EnableConfigurationProperties({DbTunnelProperties.class}) +public class DBTunnelAutoConfiguration { + + @Bean(destroyMethod = "close") + public DBTunnel dbTunnel(DbTunnelProperties dbTunnelProperties) { + return new DBTunnel(dbTunnelProperties); + } + +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DbTunnelProperties.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DbTunnelProperties.java new file mode 100644 index 0000000..bb56785 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/DbTunnelProperties.java @@ -0,0 +1,34 @@ +package com.njzscloud.common.mp.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties("mybatis-plus.tunnel") +public class DbTunnelProperties { + private boolean enable = false; + + private SSHProperties ssh; + private DBProperties db; + + @Getter + @Setter + public static class SSHProperties { + private String host; + private int port = 22; + private String user; + private String credentials; + private String passphrase; + private int localPort; + } + + @Getter + @Setter + public static class DBProperties { + private String host; + private int port = 3306; + } + +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java new file mode 100644 index 0000000..54f1cb1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java @@ -0,0 +1,42 @@ +package com.njzscloud.common.mp.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.njzscloud.common.mp.support.CustomDataPermissionHandler; +import com.njzscloud.common.mp.support.MetaObjectHandlerImpl; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Mybatis-Plus 配置 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({MybatisPlusProperties.class}) +public class MpAutoConfiguration { + + /** + * 插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor(MybatisPlusProperties mybatisPlusProperties) { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + // 分页 + mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + // 乐观锁 + // mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + // 防止全表更新删除 + mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + + // DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor(new CustomDataPermissionHandler(mybatisPlusProperties.getIgnoreTables())); + // mybatisPlusInterceptor.addInnerInterceptor(dataPermissionInterceptor); + return mybatisPlusInterceptor; + } + + @Bean + public MetaObjectHandlerImpl metaObjectHandlerImpl() { + return new MetaObjectHandlerImpl(); + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java new file mode 100644 index 0000000..7ab76c2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java @@ -0,0 +1,14 @@ +package com.njzscloud.common.mp.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@Getter +@Setter +@ConfigurationProperties("mybatis-plus") +public class MybatisPlusProperties { + private List ignoreTables; +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java new file mode 100644 index 0000000..339eb37 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java @@ -0,0 +1,43 @@ +package com.njzscloud.common.mp.support; + +import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; + +import java.util.List; +import java.util.function.Supplier; + +@Slf4j +@AllArgsConstructor +public class CustomDataPermissionHandler implements MultiDataPermissionHandler { + + private final List ignoreTables; + + public static final Supplier TENANT_ID_SUPPLIER = () -> { + UserDetail userDetail = SecurityUtil.loginUser(); + Long tenantId = 0L; + if (userDetail != null) tenantId = userDetail.getTenantId(); + if (tenantId == null) tenantId = 0L; + return tenantId; + }; + + @Override + public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { + Long tenantId = TENANT_ID_SUPPLIER.get(); + String tableName = table.getName(); + if ((ignoreTables != null && ignoreTables.contains(tableName)) || tenantId == null || tenantId == 0L) + return null; + try { + return CCJSqlParserUtil.parseCondExpression("tenant_id = " + tenantId); + } catch (JSQLParserException e) { + log.error("租户 Id 条件设置失败:{}", tenantId, e); + return null; + } + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/DBTunnel.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/DBTunnel.java new file mode 100644 index 0000000..7e4cf29 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/DBTunnel.java @@ -0,0 +1,114 @@ +package com.njzscloud.common.mp.support; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.njzscloud.common.mp.config.DbTunnelProperties; + +import java.io.File; + +import com.jcraft.jsch.*; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class DBTunnel { + + private final DbTunnelProperties dbTunnelProperties; + private JSch jsch; + private Session session; + private ScheduledExecutorService heartbeatExecutor; + + public DBTunnel(DbTunnelProperties dbTunnelProperties) { + this.dbTunnelProperties = dbTunnelProperties; + if (dbTunnelProperties.isEnable()) { + log.info("数据库 SSH 隧道已启用"); + connect(); + startHeartbeat(); + } + } + + private synchronized void connect() { + try { + if (isConnected()) { + return; + } + + DbTunnelProperties.SSHProperties ssh = dbTunnelProperties.getSsh(); + String credentials = ssh.getCredentials(); + String passphrase = ssh.getPassphrase(); + String user = ssh.getUser(); + String host = ssh.getHost(); + int port = ssh.getPort(); + int localPort = ssh.getLocalPort(); + DbTunnelProperties.DBProperties db = dbTunnelProperties.getDb(); + String dbHost = db.getHost(); + int dbPort = db.getPort(); + + jsch = new JSch(); + // 使用私钥认证 + File keyFile = new File(credentials); + if (!keyFile.exists()) { + throw new RuntimeException("私钥文件不存在: " + credentials); + } + + if (passphrase != null) { + jsch.addIdentity(credentials, passphrase); + } else { + jsch.addIdentity(credentials); + } + + // 创建SSH会话 + session = jsch.getSession(user, host, port); + session.setConfig("StrictHostKeyChecking", "no"); // 禁用主机密钥检查 + + // 设置保持活动状态 + session.setServerAliveInterval(60000); // 每分钟发送一次保持活动的数据包 + session.setServerAliveCountMax(3); // 允许3次失败 + + session.connect(); + + // 设置端口转发 (本地端口 -> 远程数据库) + session.setPortForwardingL(localPort, dbHost, dbPort); + log.info("SSH 隧道已成功连接并转发端口 {} -> {}:{}", localPort, dbHost, dbPort); + } catch (JSchException e) { + log.error("SSH 隧道连接失败", e); + throw new RuntimeException("SSH 隧道连接失败", e); + } + } + + private void startHeartbeat() { + heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(); + heartbeatExecutor.scheduleAtFixedRate(() -> { + try { + if (!isConnected()) { + log.warn("SSH 隧道连接已断开,尝试重新连接..."); + connect(); + } + } catch (Exception e) { + log.error("SSH 隧道心跳检测失败", e); + } + }, 30, 30, TimeUnit.SECONDS); // 每30秒检查一次连接状态 + } + + public boolean isConnected() { + return session != null && session.isConnected(); + } + + public void close() { + if (heartbeatExecutor != null) { + heartbeatExecutor.shutdownNow(); + } + + if (session != null && session.isConnected()) { + session.disconnect(); + log.info("SSH隧道已关闭"); + } + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java new file mode 100644 index 0000000..e2fc4d9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.mp.support; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import org.apache.ibatis.reflection.MetaObject; + +import java.time.LocalDateTime; +import java.util.function.Supplier; + +/** + * 字段填充 + * 填充字段:createTime、creatorId、modifyTime、modifierId + */ +public class MetaObjectHandlerImpl implements MetaObjectHandler { + /** + * 获取当前登录人 Id,默认:0 + */ + public static final Supplier OPERATOR_ID_SUPPLIER = () -> { + UserDetail userDetail = SecurityUtil.loginUser(); + Long userId = 0L; + if (userDetail != null) userId = userDetail.getUserId(); + if (userId == null) userId = 0L; + return userId; + }; + public static final Supplier TENANT_ID_SUPPLIER = () -> { + UserDetail userDetail = SecurityUtil.loginUser(); + Long tenantId = 0L; + if (userDetail != null) tenantId = userDetail.getTenantId(); + if (tenantId == null) tenantId = 0L; + return tenantId; + }; + + public static final Supplier OPERATOR_TIME_SUPPLIER = LocalDateTime::now; + + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", OPERATOR_TIME_SUPPLIER, LocalDateTime.class); + this.strictInsertFill(metaObject, "creatorId", OPERATOR_ID_SUPPLIER, Long.class); + + this.strictInsertFill(metaObject, "modifyTime", OPERATOR_TIME_SUPPLIER, LocalDateTime.class); + this.strictInsertFill(metaObject, "modifierId", OPERATOR_ID_SUPPLIER, Long.class); + this.strictInsertFill(metaObject, "tenantId", TENANT_ID_SUPPLIER, Long.class); + } + + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "modifyTime", OPERATOR_TIME_SUPPLIER, LocalDateTime.class); + this.strictUpdateFill(metaObject, "modifierId", OPERATOR_ID_SUPPLIER, Long.class); + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java new file mode 100644 index 0000000..7680965 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java @@ -0,0 +1,65 @@ +package com.njzscloud.common.mp.support; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +/** + * 分页参数 + */ +@Getter +@Setter +@Accessors(chain = true) +public final class PageParam { + /** + * 每页显示条数,默认 10 + */ + private Integer size; + + /** + * 当前页 + */ + private Integer current; + + /** + * 排序字段信息 + * 格式:a:desc,b:asc + */ + private String orders; + + public Page toPage() { + List orderItemList = new ArrayList<>(); + if (StrUtil.isNotBlank(orders)) { + String[] orderItems = orders.split(","); + for (String orderItem : orderItems) { + String[] item = orderItem.split(":"); + OrderItem orderItem_ = new OrderItem(); + if (item.length == 1) { + orderItem_.setColumn(item[0]); + orderItemList.add(orderItem_); + } else if (item.length == 2) { + orderItem_.setColumn(item[0]); + if ("asc".equalsIgnoreCase(item[1])) { + orderItem_.setAsc(true); + } else if ("desc".equalsIgnoreCase(item[1])) { + orderItem_.setAsc(false); + } else { + throw new RuntimeException(StrUtil.format("排序参数错误,字段:orders,接收到的值:{}", orders)); + } + orderItemList.add(orderItem_); + } else { + throw new RuntimeException(StrUtil.format("排序参数错误,字段:orders,接收到的值:{}", orders)); + } + } + } + Page page = new Page<>(current == null ? 1 : current, size == null ? 500 : size); + page.setOrders(orderItemList); + return page; + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java new file mode 100644 index 0000000..b0b950f --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.mp.support; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.List; + +/** + * 分页结果类 + */ +@Getter +@Setter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class PageResult { + + private final List records; + + /** + * 总数 + */ + private final Integer total; + + /** + * 总页数 + */ + private final Integer pages; + + /** + * 每页显示条数,默认 10 + */ + private final Integer size; + + /** + * 当前页 + */ + private final Integer current; + + + public static PageResult of(IPage page) { + return new PageResult<>(page.getRecords(), (int) page.getTotal(), (int) page.getPages(), (int) page.getSize(), (int) page.getCurrent()); + } + +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java new file mode 100644 index 0000000..85c7b83 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java @@ -0,0 +1,123 @@ +package com.njzscloud.common.mp.support.handler.e; + +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.type.EnumTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 枚举类型处理器
+ * 仅处理 Dict 类型的枚举, 其他枚举类型采用 Mybatis 默认处理器
+ * 使用配置项 mybatis-plus.configuration.default-enum-type-handler + * + * @see Dict + * @see DictInt + * @see DictStr + */ +@Slf4j +public class EnumTypeHandlerDealer> extends EnumTypeHandler { + + private final Object[] enums; + private final EnumType enumType; + + /** + * 构建枚举类型处理器 + * + * @param type 枚举类型 + * @see DictInt + * @see DictStr + */ + public EnumTypeHandlerDealer(Class type) { + super(type); + + enums = type.getEnumConstants(); + + if (DictInt.class.isAssignableFrom(type)) { + enumType = EnumType.DICT_INT; + } else if (DictStr.class.isAssignableFrom(type)) { + enumType = EnumType.DICT_STR; + } else { + enumType = EnumType.OTHER; + log.warn("枚举类型 [{}] 未实现 [{}] 或 [{}], 数据库操作将使用 Mybatis 默认处理器", type, DictInt.class.getName(), DictStr.class.getName()); + } + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { + switch (enumType) { + case OTHER: + super.setNonNullParameter(ps, i, parameter, jdbcType); + break; + case DICT_INT: + ps.setObject(i, ((DictInt) parameter).getVal()); + break; + case DICT_STR: + ps.setObject(i, ((DictStr) parameter).getVal()); + break; + } + } + + @Override + public E getNullableResult(ResultSet rs, String columnName) throws SQLException { + String strVal = rs.getString(columnName); + + if (strVal == null) { + return null; + } + + if (enumType == EnumType.OTHER) { + return super.getNullableResult(rs, columnName); + } + return getNullableResult(strVal); + } + + @Override + public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String strVal = rs.getString(columnIndex); + if (strVal == null) { + return null; + } + + if (enumType == EnumType.OTHER) { + return super.getNullableResult(rs, columnIndex); + } + return getNullableResult(strVal); + } + + @Override + public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String strVal = cs.getString(columnIndex); + + if (strVal == null) { + return null; + } + if (enumType == EnumType.OTHER) { + return super.getNullableResult(cs, columnIndex); + } + + return getNullableResult(strVal); + } + + @SuppressWarnings("unchecked") + private E getNullableResult(String strVal) { + if (enumType == EnumType.DICT_INT) { + int val = Integer.parseInt(strVal); + return (E) Dict.parse(val, (DictInt[]) enums); + } else { + return (E) Dict.parse(strVal, (DictStr[]) enums); + } + } + + private enum EnumType { + OTHER, + DICT_INT, + DICT_STR + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java new file mode 100644 index 0000000..77e9e57 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java @@ -0,0 +1,48 @@ +package com.njzscloud.common.mp.support.handler.j; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import com.njzscloud.common.core.fastjson.Fastjson; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Json 类型处理器 + */ +@Slf4j +@MappedTypes({Object.class}) +@MappedJdbcTypes({JdbcType.VARCHAR}) +public class JsonTypeHandler implements TypeHandler { + + @Override + public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { + if (parameter == null) ps.setNull(i, jdbcType.TYPE_CODE); + else ps.setString(i, Fastjson.toJsonStr(parameter)); + + } + + @Override + public Object getResult(ResultSet rs, String columnName) throws SQLException { + String result = rs.getString(columnName); + return StrUtil.isBlank(result) ? null : Fastjson.toBean(result, Object.class); + } + + @Override + public Object getResult(ResultSet rs, int columnIndex) throws SQLException { + String result = rs.getString(columnIndex); + return StrUtil.isBlank(result) ? null : Fastjson.toBean(result, Object.class); + } + + @Override + public Object getResult(CallableStatement cs, int columnIndex) throws SQLException { + String result = cs.getString(columnIndex); + return StrUtil.isBlank(result) ? null : Fastjson.toBean(result, Object.class); + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..4240fc4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.mp.config.MpAutoConfiguration,\ + com.njzscloud.common.mp.config.DBTunnelAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-mvc/pom.xml b/njzscloud-common/njzscloud-common-mvc/pom.xml new file mode 100644 index 0000000..4f6163a --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-mvc + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-security + provided + + + + jakarta.validation + jakarta.validation-api + + + + org.hibernate.validator + hibernate-validator + + + + + javax.servlet + javax.servlet-api + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java new file mode 100644 index 0000000..07e359e --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.mvc.config; + +import com.njzscloud.common.core.jackson.serializer.BigDecimalModule; +import com.njzscloud.common.core.jackson.serializer.LongModule; +import com.njzscloud.common.core.jackson.serializer.TimeModule; +import com.njzscloud.common.mvc.support.DictHandlerMethodArgumentResolver; +import com.njzscloud.common.mvc.support.GlobalExceptionController; +import com.njzscloud.common.mvc.support.ReusableRequestFilter; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Spring MVC 配置 + */ +@Configuration(proxyBeanMethods = false) +public class MvcAutoConfiguration { + /** + * Jackson 配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return build -> build + .modules(new TimeModule(), new LongModule(), new BigDecimalModule()); + } + + @Bean + public DictHandlerMethodArgumentResolver dictHandlerMethodArgumentResolver() { + return new DictHandlerMethodArgumentResolver(); + } + + @Bean + public GlobalExceptionController globalExceptionController() { + return new GlobalExceptionController(); + } + + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ReusableRequestFilter()); + registration.addUrlPatterns("/*"); + registration.setName("reusableRequestFilter"); + registration.setOrder(0); + return registration; + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java new file mode 100644 index 0000000..a740461 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.mvc.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 参数、返回值处理器配置 + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +public class RequestMappingHandlerAdapterAutoConfiguration implements InitializingBean { + + private final RequestMappingHandlerAdapter requestMappingHandlerAdapter; + + /** + * 收集自定义的 Controller 方法参数处理器 + */ + private final ObjectProvider handlerMethodArgumentResolver; + + /** + * 收集自定义的 Controller 方法返回值处理器 + */ + private final ObjectProvider handlerMethodReturnValueHandler; + + public RequestMappingHandlerAdapterAutoConfiguration(@Autowired(required = false) RequestMappingHandlerAdapter requestMappingHandlerAdapter, + ObjectProvider handlerMethodArgumentResolver, + ObjectProvider handlerMethodReturnValueHandler) { + this.requestMappingHandlerAdapter = requestMappingHandlerAdapter; + this.handlerMethodArgumentResolver = handlerMethodArgumentResolver; + this.handlerMethodReturnValueHandler = handlerMethodReturnValueHandler; + } + + @Override + public void afterPropertiesSet() throws Exception { + + if (requestMappingHandlerAdapter == null) { + log.warn("RequestMappingHandlerAdapter 为空"); + return; + } + + List handlerMethodArgumentResolverList = handlerMethodArgumentResolver.orderedStream().collect(Collectors.toList()); + List handlerMethodReturnValueHandlerList = handlerMethodReturnValueHandler.orderedStream().collect(Collectors.toList()); + + // Controller 方法参数处理器 + if (CollUtil.isNotEmpty(handlerMethodArgumentResolverList)) { + List argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers(); + if (argumentResolvers != null) { + handlerMethodArgumentResolverList.addAll(argumentResolvers); + } + requestMappingHandlerAdapter.setArgumentResolvers(handlerMethodArgumentResolverList); + } + + // Controller 方法返回值处理器 + if (CollUtil.isNotEmpty(handlerMethodReturnValueHandlerList)) { + List returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers(); + if (returnValueHandlers != null) { + handlerMethodReturnValueHandlerList.addAll(returnValueHandlers); + } + requestMappingHandlerAdapter.setReturnValueHandlers(handlerMethodReturnValueHandlerList); + } + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java new file mode 100644 index 0000000..16ac323 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java @@ -0,0 +1,60 @@ +package com.njzscloud.common.mvc.support; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * 字典枚举参数处理器 + */ +@SuppressWarnings({"ConstantConditions"}) +public class DictHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + /** + * 支持 {@link DictInt} 和 {@link DictStr} 类型的返回值 + * + * @param parameter 返回值信息 + * @return 当参数类型为 DictInt 或 DictStr 时返回 true + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return DictInt.class.isAssignableFrom(parameter.getParameterType()) || DictStr.class.isAssignableFrom(parameter.getParameterType()); + } + + /** + * 解析参数 + * + * @param parameter 参数信息 + * @param container 当前请求的模型视图容器 + * @param request 当前请求对象 + * @param factory 创建 {@link WebDataBinder} 的工厂对象 + * @return 返回 {@link DictInt} 或 {@link DictStr} 对象 + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) throws Exception { + String parameterName = parameter.getParameterName(); + String param = request.getParameter(parameterName); + + if (StrUtil.isBlank(param)) return null; + + Class type = parameter.getParameterType(); + + if (DictInt.class.isAssignableFrom(type)) { + Integer val = Integer.parseInt(param); + DictInt[] constants = (DictInt[]) type.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictStr.class.isAssignableFrom(type)) { + DictStr[] constants = (DictStr[]) type.getEnumConstants(); + return Dict.parse(param, constants); + } else { + return null; + } + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java new file mode 100644 index 0000000..76dd19b --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java @@ -0,0 +1,311 @@ +package com.njzscloud.common.mvc.support; + +import com.njzscloud.common.core.ex.CliException; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.ex.SysException; +import com.njzscloud.common.core.utils.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.validation.BindException; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 异常处理器 + * + * @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionController { + + /** + * 请求方法不匹配 + */ + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public R httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) { + printRequest(); + String method = e.getMethod(); + String[] supportedMethods = e.getSupportedMethods(); + log.error("不支持的请求方法:【{}】仅支持:【{}】", method, supportedMethods, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "不支持的请求方法:【{}】仅支持:【{}】", method, supportedMethods); + } + + /** + * 内容类型不匹配 + */ + @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public R httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) { + printRequest(); + MediaType contentType = e.getContentType(); + List supportedMediaTypes = e.getSupportedMediaTypes(); + log.error("不支持的内容类型:【{}】仅支持:【{}】", contentType, supportedMediaTypes, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "不支持的内容类型:【{}】仅支持:【{}】", contentType, supportedMediaTypes); + } + + /** + * 未发现多部分表单 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestPartException.class) + public R missingServletRequestPartExceptionHandler(MissingServletRequestPartException e) { + printRequest(); + String requestPartName = e.getRequestPartName(); + log.error("未发现多部分表单, 字段:【{}】", requestPartName, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "未发现多部分表单, 字段:【{}】", requestPartName); + } + + /** + * 缺少必要参数, 使用 @RequestParam(required = true) 的方法出现的异常 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestParameterException.class) + public R missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) { + printRequest(); + String parameterName = e.getParameterName(); + log.error("缺少必要参数, 字段:【{}】必须有值", parameterName, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "缺少必要参数, 字段:【{}】必须有值", parameterName); + } + + /** + * 缺少必要参数, 使用 @RequestHeader(required = true) 的方法出现的异常 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingRequestHeaderException.class) + public R missingRequestHeaderExceptionHandler(MissingRequestHeaderException e) { + printRequest(); + String headerName = e.getHeaderName(); + log.error("缺少必要的请求头参数, 请求头:【{}】必须有值", headerName, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "缺少必要的请求头参数, 请求头:【{}】必须有值", headerName); + } + + /** + * 缺少必要的请求参数 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(ServletRequestBindingException.class) + public R servletRequestBindingExceptionHandler(ServletRequestBindingException e) { + printRequest(); + log.error("缺少必要的请求参数", e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "缺少必要参数,【{}】", e.getMessage()); + } + + /** + * 方法参数类型与给定的参数类型不匹配 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public R methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException e) { + printRequest(); + String name = e.getName(); + Object value = e.getValue(); + Class requiredType = e.getRequiredType(); + String simpleName = null; + if (requiredType != null) { + simpleName = requiredType.getSimpleName(); + } + log.error("参数类型不匹配, 字段:【{}】需要的类型为:【{}】, 接收到的值为:【{}】", name, simpleName, value, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "参数类型不匹配, 字段:【{}】需要的类型为:【{}】, 接收到的值为:【{}】", name, simpleName, value); + } + + + /** + * 读取请求参数失败,可能为参数格式错误,POST 请求发送 JSON 数据, 数据类型不匹配、时间格式不正确等 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public R httpMessageNotReadableExceptionHandler(HttpMessageNotReadableException e) { + printRequest(); + log.error("读取请求体参数失败, 可能是参数格式错误", e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "读取请求体参数失败, 可能是参数格式错误.【{}】", e.getMessage()); + } + + /** + * 注解 @RequestBody 和 @Validated 标注的对象,校验失败 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public R methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { + printRequest(); + List allErrors = e.getAllErrors(); + List msg = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()); + log.error("参数校验失败:【{}】", msg, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, msg); + } + + /** + * GET 请求参数校验失败(格式、数据类型错误) + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(BindException.class) + public R bindExceptionHandler(BindException e) { + printRequest(); + List allErrors = e.getAllErrors(); + List msg = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()); + log.error("参数校验失败:【{}】", msg, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, msg); + } + + + /** + * 404 + *

需要配置:

+ *
+     * spring:
+     *   web:
+     *     resources:
+     *       # 关闭静态资源映射
+     *       add-mappings: false
+     *   mvc:
+     *     # 为找到的资源抛出异常
+     *     throw-exception-if-no-handler-found: true
+     */
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(NoHandlerFoundException.class)
+    public R noHandlerFoundExceptionHandler(NoHandlerFoundException e) {
+        printRequest();
+        String httpMethod = e.getHttpMethod();
+        String requestURL = e.getRequestURL();
+        log.error("资源不存在:[【{}】【{}】]", httpMethod, requestURL, e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, "资源不存在,【{}】【{}】", httpMethod, requestURL);
+    }
+
+    /**
+     * 无法生成需要的响应结果
+     */
+    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
+    @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
+    public R httpMediaTypeNotAcceptableExceptionHandler(HttpMediaTypeNotAcceptableException e) {
+        printRequest();
+        log.error("无法生成需要的响应结果", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "无法生成需要的响应结果,{}", e.getMessage());
+    }
+
+    /**
+     * 路径参数名称不匹配
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(MissingPathVariableException.class)
+    public R missingPathVariableExceptionHandler(MissingPathVariableException e) {
+        printRequest();
+        String variableName = e.getVariableName();
+        log.error("路径参数名称与 URL 模板不匹配, 需要的路径参数名称:【{}】, URL 模板中未找到此名称对应的参数", variableName, e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, "路径参数名称与 URL 模板不匹配, 需要的路径参数名称:【{}】, URL 模板中未找到此名称对应的参数", variableName);
+    }
+
+    /**
+     * 没有合适的转换器
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(ConversionNotSupportedException.class)
+    public R conversionNotSupportedExceptionHandler(ConversionNotSupportedException e) {
+        printRequest();
+        log.error("没有合适的转换器", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "没有合适的转换器,【{}】", e.getMessage());
+    }
+
+    /**
+     * 写出响应信息时出现错误
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(HttpMessageNotWritableException.class)
+    public R httpMessageNotWritableExceptionHandler(HttpMessageNotWritableException e) {
+        printRequest();
+        log.error("写出响应信息时出现错误", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "响应时出现错误,【{}】", e.getMessage());
+    }
+
+    /**
+     * 异步请求超时
+     */
+    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
+    @ExceptionHandler(AsyncRequestTimeoutException.class)
+    public R asyncRequestTimeoutExceptionHandler(AsyncRequestTimeoutException e) {
+        printRequest();
+        log.error("下游服务响应超时", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "服务器错误, 下游服务响应超时。{}", e.getMessage());
+    }
+
+    /**
+     * 参数错误
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(IllegalArgumentException.class)
+    public R cliExceptionHandler(IllegalArgumentException e) {
+        printRequest();
+        log.error("客户端参数错误", e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, e.getMessage());
+    }
+
+    /**
+     * 客户端错误
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(CliException.class)
+    public R cliExceptionHandler(CliException e) {
+        printRequest();
+        log.error(e.getMessage(), e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, e.message);
+    }
+
+    /**
+     * 系统异常
+     */
+    @ResponseStatus(HttpStatus.OK)
+    @ExceptionHandler(SysException.class)
+    public R sysExceptionHandler(SysException e) {
+        printRequest();
+        log.error(e.getMessage(), e);
+        return R.failed(e.expect, e.msg, e.message);
+    }
+
+    /**
+     * 服务器错误
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(Throwable.class)
+    public R exceptionHandler(Throwable e) {
+        printRequest();
+        log.error("内部服务错误", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "内部服务错误");
+    }
+
+    /**
+     * 打印日志
+     */
+    private void printRequest() {
+        try {
+            RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
+            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+            String method = request.getMethod();
+            String requestURI = request.getRequestURI();
+            log.error("接口请求异常:【{}】【{}】", method, requestURI);
+        } catch (Throwable e) {
+            log.error("接口请求信息打印失败", e);
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java
new file mode 100644
index 0000000..6de1a10
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java
@@ -0,0 +1,146 @@
+package com.njzscloud.common.mvc.support;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import com.njzscloud.common.core.jackson.Jackson;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
+import org.jetbrains.annotations.NotNull;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+public class ReusableHttpServletRequest extends HttpServletRequestWrapper {
+    private final byte[] body;
+
+    public ReusableHttpServletRequest(HttpServletRequest request) {
+        super(request);
+        try {
+            if (ServletFileUpload.isMultipartContent(request)) {
+                body = "多部分表单".getBytes(StandardCharsets.UTF_8);
+            } else {
+                ServletInputStream inputStream = request.getInputStream();
+                body = IoUtil.readBytes(inputStream);
+            }
+            printRequest();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void printRequest() {
+        try {
+            String method = this.getMethod();
+            String requestURI = this.getRequestURI();
+            Map parameterMap = this.getParameterMap();
+            Enumeration headerNames = this.getHeaderNames();
+            Map headerMap = new HashMap<>();
+            while (headerNames.hasMoreElements()) {
+                String headerName = headerNames.nextElement();
+                String header = this.getHeader(headerName);
+                headerMap.put(headerName, header);
+            }
+            String body;
+            if (method.equalsIgnoreCase("GET")) {
+                body = "无";
+            } else {
+                if (this.body == null || this.body.length == 0) {
+                    body = "无";
+                } else {
+                    body = new String(this.body, StandardCharsets.UTF_8);
+                    body = StrUtil.isBlank(body) ? "无" : body;
+                }
+            }
+            log.info("接口:【{}】【{}】\n请求头:【{}】\n请求参数:【{}】\n请求体:【{}】",
+                    method, requestURI, headerMap.isEmpty() ? "无" : Jackson.toJsonStr(headerMap), parameterMap.isEmpty() ? "无" : Jackson.toJsonStr(parameterMap), body);
+        } catch (Throwable e) {
+            log.error("接口请求信息打印失败", e);
+        }
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
+        return new ServletInputStream() {
+            @Override
+            public int read() throws IOException {
+                return bais.read();
+            }
+
+            @Override
+            public boolean isFinished() {
+                return false;
+            }
+
+            @Override
+            public boolean isReady() {
+                return true;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener) {
+            }
+
+            @Override
+            public int readLine(byte[] b, int off, int len) throws IOException {
+                return super.readLine(b, off, len);
+            }
+
+            @Override
+            public int read(@NotNull byte[] b) throws IOException {
+                return bais.read(b);
+            }
+
+            @Override
+            public int read(@NotNull byte[] b, int off, int len) throws IOException {
+                return bais.read(b, off, len);
+            }
+
+            @Override
+            public long skip(long n) throws IOException {
+                return bais.skip(n);
+            }
+
+            @Override
+            public int available() throws IOException {
+                return bais.available();
+            }
+
+            @Override
+            public void close() throws IOException {
+                bais.close();
+            }
+
+            @Override
+            public synchronized void mark(int readlimit) {
+                bais.mark(readlimit);
+            }
+
+            @Override
+            public synchronized void reset() throws IOException {
+                bais.reset();
+            }
+
+            @Override
+            public boolean markSupported() {
+                return bais.markSupported();
+            }
+        };
+    }
+
+    @Override
+    public BufferedReader getReader() throws IOException {
+        return new BufferedReader(new InputStreamReader(getInputStream()));
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java
new file mode 100644
index 0000000..4c928fe
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java
@@ -0,0 +1,17 @@
+package com.njzscloud.common.mvc.support;
+
+import lombok.extern.slf4j.Slf4j;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+@Slf4j
+public class ReusableRequestFilter implements Filter {
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        chain.doFilter(new ReusableHttpServletRequest((HttpServletRequest) request), response);
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/FileResponseUtil.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/FileResponseUtil.java
new file mode 100644
index 0000000..4c99b94
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/FileResponseUtil.java
@@ -0,0 +1,97 @@
+package com.njzscloud.common.mvc.util;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import com.njzscloud.common.core.ex.Exceptions;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.InputStream;
+
+/**
+ * 响应文件工具
+ * (操作完成后输入输出流会被关闭)
+ */
+@Slf4j
+public class FileResponseUtil {
+    /**
+     * 预览文件
+     *
+     * @param response    HttpServletResponse
+     * @param inputStream 文件流
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void preview(HttpServletResponse response, InputStream inputStream, String contentType, String fileName) {
+        FileResponseUtil.writeFile(response, inputStream, contentType, "inline" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 预览文件
+     *
+     * @param response    HttpServletResponse
+     * @param bytes       文件数据
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void preview(HttpServletResponse response, byte[] bytes, String contentType, String fileName) {
+        FileResponseUtil.writeFile(response, bytes, contentType, "inline" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 下载文件
+     *
+     * @param response    HttpServletResponse
+     * @param inputStream 文件流
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void download(HttpServletResponse response, InputStream inputStream, String contentType, String fileName) {
+        FileResponseUtil.writeFile(response, inputStream, contentType, "attachment" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 下载文件
+     *
+     * @param response    HttpServletResponse
+     * @param bytes       文件数据
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void download(HttpServletResponse response, byte[] bytes, String contentType, String fileName) {
+        FileResponseUtil.writeFile(response, bytes, contentType, "attachment" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 向客户端响应文件
+     *
+     * @param response           HttpServletResponse
+     * @param data               文件内容, 只能是 InputStream 或 byte[]
+     * @param contentType        MIME
+     * @param contentDisposition 请求头 Content-Disposition
+     */
+    public static void writeFile(HttpServletResponse response, Object data, String contentType, String contentDisposition) {
+        ServletOutputStream outputStream = null;
+        try {
+            response.setContentType(contentType);
+            response.setCharacterEncoding(CharsetUtil.UTF_8);
+            response.setHeader("Content-Disposition", contentDisposition);
+            outputStream = response.getOutputStream();
+            if (data instanceof InputStream) {
+                IoUtil.copy((InputStream) data, outputStream);
+            } else if (data instanceof byte[]) {
+                IoUtil.write(outputStream, false, (byte[]) data);
+            }
+        } catch (Exception e) {
+            throw Exceptions.error(e, "文件响应失败");
+        } finally {
+            IoUtil.close(outputStream);
+            if (data instanceof InputStream) {
+                IoUtil.close((InputStream) data);
+            }
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java
new file mode 100644
index 0000000..1574587
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java
@@ -0,0 +1,5 @@
+package com.njzscloud.common.mvc.validator;
+
+public interface Constrained {
+    ValidRule[] rules();
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java
new file mode 100644
index 0000000..b87ebbc
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java
@@ -0,0 +1,17 @@
+package com.njzscloud.common.mvc.validator;
+
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@javax.validation.Constraint(validatedBy = ConstraintValidator.class)
+@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
+public @interface Constraint {
+    String message() default "";
+
+    Class[] groups() default {};
+
+    Class[] payload() default {};
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java
new file mode 100644
index 0000000..32697be
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java
@@ -0,0 +1,31 @@
+package com.njzscloud.common.mvc.validator;
+
+import cn.hutool.core.util.StrUtil;
+
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ConstraintValidator implements javax.validation.ConstraintValidator {
+
+    @Override
+    public boolean isValid(Constrained valid, ConstraintValidatorContext ctx) {
+        ValidRule[] rules = valid.rules();
+
+        if (rules == null || rules.length == 0) return true;
+
+        List messageList = Arrays.stream(rules)
+                .filter(it -> !it.predicate.get())
+                .map(it -> it.message)
+                .collect(Collectors.toList());
+
+        if (messageList.isEmpty()) return true;
+
+        ctx.disableDefaultConstraintViolation();
+        ctx.buildConstraintViolationWithTemplate(StrUtil.join(";\n", messageList))
+                .addConstraintViolation();
+
+        return false;
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java
new file mode 100644
index 0000000..5fe392e
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java
@@ -0,0 +1,22 @@
+package com.njzscloud.common.mvc.validator;
+
+
+import java.util.function.Supplier;
+
+/**
+ * 校验规则
+ */
+public class ValidRule {
+    public final Supplier predicate;
+    public final String message;
+
+    private ValidRule(Supplier predicate, String message) {
+        this.predicate = predicate;
+        this.message = message;
+    }
+
+    public static ValidRule of(Supplier predicate, String message) {
+        return new ValidRule(predicate, message);
+    }
+}
+
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketAutoConfiguration.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketAutoConfiguration.java
new file mode 100644
index 0000000..8ebebb6
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketAutoConfiguration.java
@@ -0,0 +1,75 @@
+package com.njzscloud.common.mvc.ws.config;
+
+import com.njzscloud.common.mvc.ws.support.TokenHandshakeInterceptor;
+import com.njzscloud.common.mvc.ws.support.WebSocketChannelInterceptor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+/**
+ * Websocket 配置,使用 STOMP 协议
+ */
+@Configuration(proxyBeanMethods = false)
+@RequiredArgsConstructor
+@EnableWebSocketMessageBroker
+@EnableConfigurationProperties(WebsocketProperties.class)
+@ConditionalOnProperty(value = "websocket.enable", havingValue = "true")
+public class WebsocketAutoConfiguration implements WebSocketMessageBrokerConfigurer, DisposableBean {
+
+    private final WebsocketProperties websocketProperties;
+    private final WebSocketChannelInterceptor webSocketChannelInterceptor = new WebSocketChannelInterceptor();
+    private final TokenHandshakeInterceptor tokenHandshakeInterceptor = new TokenHandshakeInterceptor();
+
+    /**
+     * 用于心跳消息的线程池
+     */
+    private final ThreadPoolTaskScheduler tts = new ThreadPoolTaskScheduler();
+
+    {
+
+        tts.setPoolSize(1);
+        tts.setThreadNamePrefix("ws-heartbeat-thread-");
+        tts.initialize();
+    }
+
+    /**
+     * 设置 Websocket
+     */
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        registry.addEndpoint(websocketProperties.getEndpoint())
+                .addInterceptors(tokenHandshakeInterceptor);
+    }
+
+    /**
+     * 配置 STOMP 消息服务器
+     */
+    @Override
+    public void configureMessageBroker(MessageBrokerRegistry config) {
+        WebsocketProperties.StompProperties stomp = websocketProperties.getStomp();
+        config.setApplicationDestinationPrefixes(stomp.getApplicationPrefixes())
+                .enableSimpleBroker(stomp.getBroadcastPrefixes())
+                .setTaskScheduler(tts);
+    }
+
+    /**
+     * 消息拦截器
+     */
+    @Override
+    public void configureClientInboundChannel(ChannelRegistration registration) {
+        registration.interceptors(webSocketChannelInterceptor);
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        tts.shutdown();
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketProperties.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketProperties.java
new file mode 100644
index 0000000..467921f
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/config/WebsocketProperties.java
@@ -0,0 +1,43 @@
+package com.njzscloud.common.mvc.ws.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Websocket 配置项
+ */
+@Getter
+@Setter
+@ConfigurationProperties("websocket")
+public class WebsocketProperties {
+    /**
+     * 是否启用 Websocket
+     */
+    private boolean enable;
+
+    /**
+     * Websocket 连接地址,默认:/ws
+     */
+    private String endpoint = "/ws";
+
+    /**
+     * STOMP 协议配置
+     */
+    private StompProperties stomp = new StompProperties();
+
+    @Getter
+    @Setter
+    public static class StompProperties {
+        /**
+         * 客户端发送消息的地址前缀
+         */
+        private String[] applicationPrefixes = new String[]{"/app"};
+
+        /**
+         * 广播地址前缀
+         */
+        private String[] broadcastPrefixes = new String[]{"/topic"};
+    }
+
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/TokenHandshakeInterceptor.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/TokenHandshakeInterceptor.java
new file mode 100644
index 0000000..9252af8
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/TokenHandshakeInterceptor.java
@@ -0,0 +1,43 @@
+package com.njzscloud.common.mvc.ws.support;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+/**
+ * websocket Token 拦截器
+ * Sec-WebSocket-Protocol: [TOKEN]
+ * TOKEN 已在 Security 中校验过,此处不校验,仅配合 Websocket 完成子协议回写 + */ +@Slf4j +public class TokenHandshakeInterceptor implements HandshakeInterceptor { + private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map attributes) { + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + try { + HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest(); + HttpServletResponse httpResponse = ((ServletServerHttpResponse) response).getServletResponse(); + String token = httpRequest.getHeader(SEC_WEB_SOCKET_PROTOCOL); + if (StrUtil.isNotEmpty(token)) { + httpResponse.addHeader(SEC_WEB_SOCKET_PROTOCOL, token); + } + } catch (ClassCastException e) { + log.error("[Websocket 拦截器] 类型转换失败: {}、{}", request.getClass(), response.getClass(), e); + } + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/WebSocketChannelInterceptor.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/WebSocketChannelInterceptor.java new file mode 100644 index 0000000..c839f04 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/support/WebSocketChannelInterceptor.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.mvc.ws.support; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserDetail; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; + +import java.security.Principal; + +/** + * 消息拦截器 + */ +@Slf4j +public class WebSocketChannelInterceptor implements ChannelInterceptor { + + /** + * 消息发送之前 + */ + @Override + public Message preSend(Message message, MessageChannel messageChannel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + + // 心跳消息 + Object header = accessor.getHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER); + if (SimpMessageType.HEARTBEAT.name().equals(header)) return message; + + Principal user = accessor.getUser(); + + Assert.notNull(user, () -> Exceptions.exception("未登录,不能发送消息")); + + UserDetail userDetail = (UserDetail) user; + Token token = userDetail.getToken(); + + Assert.isFalse(token.isExpired(), () -> Exceptions.exception("登录已过期,不能发送消息")); + + return message; + } +} + diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/util/WsUtil.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/util/WsUtil.java new file mode 100644 index 0000000..fdb8bf5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/ws/util/WsUtil.java @@ -0,0 +1,33 @@ +package com.njzscloud.common.mvc.ws.util; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +public class WsUtil { + private static final SimpMessagingTemplate SIMP_MESSAGING_TEMPLATE; + + static { + SIMP_MESSAGING_TEMPLATE = SpringUtil.getBean(SimpMessagingTemplate.class); + } + + /** + * 对单发送信息 + * + * @param userId 用户 Id + * @param destination 事件 + * @param message 消息内容 + */ + public static void send(long userId, String destination, Object message) { + SIMP_MESSAGING_TEMPLATE.convertAndSendToUser(userId + "", destination, message); + } + + /** + * 对群发送信息 + * + * @param destination 事件 + * @param message 消息内容 + */ + public static void send(String destination, Object message) { + SIMP_MESSAGING_TEMPLATE.convertAndSend(destination, message); + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d30ceb2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.mvc.ws.config.WebsocketAutoConfiguration,\ + com.njzscloud.common.mvc.config.MvcAutoConfiguration,\ + com.njzscloud.common.mvc.config.RequestMappingHandlerAdapterAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-redis/pom.xml b/njzscloud-common/njzscloud-common-redis/pom.xml new file mode 100644 index 0000000..2077b16 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-redis + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + + io.lettuce + lettuce-core + + + + + org.apache.commons + commons-pool2 + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java new file mode 100644 index 0000000..bfb5fe0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java @@ -0,0 +1,232 @@ +package com.njzscloud.common.redis; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.fastjson.Fastjson; +import com.njzscloud.common.redis.support.RedisFastjsonCodec; +import com.njzscloud.common.redis.support.RedisMessageDispatch; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.TransactionResult; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.pubsub.RedisPubSubListener; +import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; +import io.lettuce.core.support.ConnectionPoolSupport; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Redis 客户端,对 lettuce 客户端的封装 + */ +@Slf4j +@SuppressWarnings("unchecked") +public class RedisCli { + + private final RedisCodec codec = new RedisFastjsonCodec(); + + /** + * lettuce 客户端 + */ + @Getter + private final RedisClient redisClient; + + /** + * 普通连接池 + */ + private final GenericObjectPool> pool; + + /** + * 发布订阅模式连接池 + */ + private final GenericObjectPool> pubSubPool; + + /** + * 已订阅的频道模式 + */ + private final Set subscribedPatterns = new HashSet<>(); + /** + * 已订阅的频道 + */ + private final Set subscribedChannels = new HashSet<>(); + + /** + * 创建 Redis 客户端 + * + * @param uri Redis 连接 URI + * @param poolConfig 连接池配置 + * @param pubsub 是否开启发布订阅模式支持 + */ + public RedisCli(RedisURI uri, GenericObjectPoolConfig poolConfig, boolean pubsub) { + redisClient = RedisClient.create(uri); + + pool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(codec), (GenericObjectPoolConfig>) poolConfig); + if (pubsub) pubSubPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connectPubSub(codec), (GenericObjectPoolConfig>) poolConfig); + else pubSubPool = null; + } + + /** + * 退出 + */ + public void exit() { + if (pubSubPool != null) { + punsubscribe(subscribedPatterns); + unsubscribe(subscribedChannels); + pubSubPool.close(); + } + pool.close(); + redisClient.shutdown(); + } + + /** + * 执行 Redis 命令 + * + * @param fn 要执行的操作 + * @param Redis 值类型 + * @param 返回值类型 + * @return 返回 fn 的返回值 + */ + public R exec(Function, R> fn) { + try (StatefulRedisConnection connection = pool.borrowObject()) { + return fn.apply((RedisCommands) connection.sync()); + } catch (Exception e) { + throw Exceptions.error(e, "Redis 操作执行失败"); + } + } + + /** + * 执行 Redis 命令(带事务) + * + * @param fn 要执行的操作 + * @param Redis 值类型 + * @return {@link TransactionResult} 事务结果 + */ + public TransactionResult execWithTransaction(Consumer> fn) { + try (StatefulRedisConnection connection = pool.borrowObject()) { + RedisCommands commands = (RedisCommands) connection.sync(); + commands.multi(); + fn.accept(commands); + return commands.exec(); + } catch (Exception e) { + throw Exceptions.error(e, "Redis 操作执行失败"); + } + } + + /** + * 添加监听器 + * + * @see RedisMessageDispatch + */ + public void addListener(RedisPubSubListener redisPubSubListener) { + try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) { + connection.addListener(redisPubSubListener); + } catch (Exception e) { + throw Exceptions.error(e, "Redis 监听器注册失败"); + } + } + + /** + * 发布消息 + * + * @param channel 频道 + * @param message 消息内容 + */ + public void publish(String channel, Object message) { + if (log.isDebugEnabled()) { + log.debug("发布消息,频道:【{}】,消息:【{}】", channel, Fastjson.toJsonStr(message)); + } + try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) { + connection.sync().publish(channel, message); + } catch (Exception e) { + throw Exceptions.error(e, "Redis 消息发布失败,频道:【{}】,消息:【{}】", channel, Fastjson.toJsonStr(message)); + } + } + + /** + * 订阅 + * + * @param channels 频道 + */ + public void subscribe(Collection channels) { + if (log.isDebugEnabled()) { + log.debug("订阅,频道:【{}】", channels); + } + Collection newChannels = CollUtil.subtract(CollUtil.distinct(channels), subscribedChannels); + try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) { + if (newChannels.isEmpty()) return; + connection.sync().subscribe(ArrayUtil.toArray(newChannels, String.class)); + subscribedChannels.addAll(newChannels); + if (log.isDebugEnabled()) { + log.debug("当前已订阅的频道:【{}】", subscribedChannels); + } + } catch (Exception e) { + throw Exceptions.error(e, "Redis 订阅失败,频道:【{}】", channels); + } + } + + /** + * 取消订阅 + * + * @param channels 频道 + */ + public void unsubscribe(Collection channels) { + if (log.isDebugEnabled()) { + log.debug("订阅,频道:【{}】", channels); + } + if (channels == null || channels.isEmpty()) return; + try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) { + connection.sync().unsubscribe(ArrayUtil.toArray(channels, String.class)); + } catch (Exception e) { + throw Exceptions.error(e, "Redis 取消订阅失败,频道:【{}】", channels); + } + } + + /** + * 订阅(使用匹配模式) + * + * @param patterns 频道匹配模式(支持 glob 匹配规则) + */ + public void psubscribe(Collection patterns) { + if (log.isDebugEnabled()) { + log.debug("订阅(使用匹配模式),频道:【{}】", patterns); + } + Collection newChannels = CollUtil.subtract(CollUtil.distinct(patterns), subscribedPatterns); + try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) { + if (newChannels.isEmpty()) return; + connection.sync().psubscribe(ArrayUtil.toArray(newChannels, String.class)); + subscribedPatterns.addAll(newChannels); + if (log.isDebugEnabled()) { + log.debug("当前已订阅的频道(使用匹配模式):【{}】", subscribedPatterns); + } + } catch (Exception e) { + throw Exceptions.error(e, "Redis 订阅失败(使用匹配模式),频道:【{}】", patterns); + } + } + + /** + * 取消订阅(使用匹配模式) + * + * @param patterns 频道匹配模式(支持 glob 匹配规则) + */ + public void punsubscribe(Collection patterns) { + if (log.isDebugEnabled()) { + log.debug("取消订阅(使用匹配模式),频道:【{}】", patterns); + } + if (patterns == null || patterns.isEmpty()) return; + try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) { + connection.sync().punsubscribe(ArrayUtil.toArray(patterns, String.class)); + } catch (Exception e) { + throw Exceptions.error(e, "Redis 取消订阅失败,(使用匹配模式),频道:【{}】", patterns); + } + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java new file mode 100644 index 0000000..b494b10 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java @@ -0,0 +1,38 @@ +package com.njzscloud.common.redis.annotation; + +/** + * Redis 监听器使用示例, + * 在类上使用 @RedisListener 在方法上使用 @RedisChannel + */ +// @RedisListener +public class Eg { + /** + * 无参 + * 注解 @RedisChannel 的参数二选一 + */ + @RedisChannel(patterns = {"a*"}, channels = {"aa"}) + public void a1() { + } + + /** + * 一个参数 + * 注解 @RedisChannel 的参数二选一 + * + * @param message 消息内容(类型不限) + */ + @RedisChannel(patterns = {"a*"}, channels = {"aa"}) + public void a2(Object message) { + } + + /** + * 三个参数 + * 注解 @RedisChannel 的参数二选一 + * + * @param pattern 频道匹配模式 + * @param channel 频道名称 + * @param message 消息内容 (类型不限) + */ + @RedisChannel(patterns = {"a*"}, channels = {"aa"}) + public void a3(String pattern, String channel, Object message) { + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java new file mode 100644 index 0000000..0040de9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java @@ -0,0 +1,33 @@ +package com.njzscloud.common.redis.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Redis 订阅通道 + *

使用方式 {Eg}

+ */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RedisChannel { + /** + * 匹配模式 + * 支持的 glob 样式模式: + * h?llo订阅 和hellohallohxllo + * h*llo订阅和hlloheeeello + * h[ae]llo订阅 和 但不订阅hellohallo,hillo + * + * @return String[] 匹配模式 + */ + String[] patterns() default {}; + + /** + * 频道名称 + * + * @return String[] 频道名称 + */ + String[] channels() default {}; +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java new file mode 100644 index 0000000..252f71a --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.redis.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * Redis 监听器 + *

使用方式 {Eg}

+ */ +@Inherited +@Component +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface RedisListener { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java new file mode 100644 index 0000000..6e00853 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.redis.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Redis 配置信息 + */ +@Getter +@Setter +@ConfigurationProperties("spring.redis") +public class RedisOtherProperties { + /** + * 是否启用 Redis + */ + private boolean enable = false; + /** + * 是否使用发布订阅功能 + */ + private boolean pubsub = false; +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java new file mode 100644 index 0000000..ca20f03 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java @@ -0,0 +1,120 @@ +package com.njzscloud.common.redis.config; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.redis.RedisCli; +import com.njzscloud.common.redis.annotation.RedisChannel; +import com.njzscloud.common.redis.annotation.RedisListener; +import com.njzscloud.common.redis.support.RedisMessageDispatch; +import io.lettuce.core.RedisURI; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; + +import java.time.Duration; +import java.util.Map; + +/** + * Redis 配置类 + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.redis.enable", havingValue = "true") +@EnableConfigurationProperties({RedisProperties.class, RedisOtherProperties.class}) +public class RedisServiceAutoConfiguration { + + /** + * Redis 客户端配置 + * + * @param redisProperties 配置(Spring 自带的配置) + * @param redisOtherProperties 配置(自定义配置) + * @return {@link RedisCli} + */ + @Bean(destroyMethod = "exit") + public RedisCli redisCli(RedisProperties redisProperties, RedisOtherProperties redisOtherProperties) { + String url = redisProperties.getUrl(); + RedisURI redisURI; + if (url == null || url.isEmpty()) { + redisURI = new RedisURI(); + redisURI.setSsl(redisProperties.isSsl()); + String username = redisProperties.getUsername(); + if (StrUtil.isNotBlank(username)) redisURI.setUsername(username); + String password = redisProperties.getPassword(); + if (StrUtil.isNotBlank(password)) redisURI.setPassword(password.toCharArray()); + + redisURI.setHost(redisProperties.getHost()); + redisURI.setPort(redisProperties.getPort()); + redisURI.setDatabase(redisProperties.getDatabase()); + Duration timeout = redisProperties.getTimeout(); + if (timeout != null) redisURI.setTimeout(timeout); + String clientName = redisProperties.getClientName(); + if (StrUtil.isNotBlank(clientName)) redisURI.setClientName(clientName); + } else { + redisURI = RedisURI.create(url); + } + + RedisProperties.Lettuce lettuce = redisProperties.getLettuce(); + RedisProperties.Pool lettucePool = lettuce.getPool(); + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMaxTotal(lettucePool.getMaxActive()); + poolConfig.setMaxIdle(lettucePool.getMaxIdle()); + poolConfig.setMinIdle(lettucePool.getMinIdle()); + Duration timeBetweenEvictionRuns = lettucePool.getTimeBetweenEvictionRuns(); + if (timeBetweenEvictionRuns != null) poolConfig.setTimeBetweenEvictionRuns(timeBetweenEvictionRuns); + + Duration maxWait = lettucePool.getMaxWait(); + if (maxWait != null) poolConfig.setMaxWait(maxWait); + + + return new RedisCli(redisURI, poolConfig, redisOtherProperties.isPubsub()); + } + + + /** + * 发布订阅模式的配置 + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "spring.redis.pubsub", havingValue = "true") + public static class RedisPubSubConfiguration { + + /** + * 配置消息派发器 + * + * @param redisCli Redis 客户端 {@link RedisCli} + * @return {@link RedisMessageDispatch} + */ + @Bean + public RedisMessageDispatch redisMessageDispatch(RedisCli redisCli) { + return new RedisMessageDispatch(redisCli); + } + + /** + * 配置 Spring 事件监听器,当触发 Spring 容器刷新事件{@link ContextRefreshedEvent}时,注册自定义的 Redis 监听器 + * + * @param redisMessageDispatch 消息派发器 + * @return {@link ApplicationListener} + * @see RedisListener + * @see RedisChannel + */ + @Bean + public ApplicationListener applicationListener(RedisMessageDispatch redisMessageDispatch) { + return event -> { + Map beansWithAnnotation = event + .getApplicationContext() + .getBeansWithAnnotation(RedisListener.class); + beansWithAnnotation.forEach((k, v) -> { + if (log.isDebugEnabled()) { + log.debug("发现 Redis 监听器:【{}】", v.getClass().getName()); + } + redisMessageDispatch.subscribe(v); + }); + }; + } + } + +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java new file mode 100644 index 0000000..49066d5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java @@ -0,0 +1,63 @@ +package com.njzscloud.common.redis.support; + +import com.njzscloud.common.core.fastjson.Fastjson; +import io.lettuce.core.codec.RedisCodec; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * Redis 序列化/反序列化 器(采用 Fastjson,已开启自动类型处理) + */ +public class RedisFastjsonCodec implements RedisCodec { + private static final byte[] EMPTY = new byte[0]; + + @Override + public String decodeKey(ByteBuffer bytes) { + return Unpooled.wrappedBuffer(bytes).toString(StandardCharsets.UTF_8); + } + + @Override + public Object decodeValue(ByteBuffer bytes) { + String jsonStr = Unpooled.wrappedBuffer(bytes).toString(StandardCharsets.UTF_8); + return Fastjson.toBean(jsonStr, Object.class); + } + + @Override + public ByteBuffer encodeKey(String key) { + if (key == null) { + return ByteBuffer.wrap(EMPTY); + } + int utf8MaxBytes = ByteBufUtil.utf8MaxBytes(key); + + ByteBuffer buffer = ByteBuffer.allocate(utf8MaxBytes); + + ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer); + byteBuf.clear(); + ByteBufUtil.writeUtf8(byteBuf, key); + buffer.limit(byteBuf.writerIndex()); + + return buffer; + } + + @Override + public ByteBuffer encodeValue(Object value) { + String jsonStr = Fastjson.toJsonStr(value); + if (jsonStr == null) { + return ByteBuffer.wrap(EMPTY); + } + int utf8MaxBytes = ByteBufUtil.utf8MaxBytes(jsonStr); + + ByteBuffer buffer = ByteBuffer.allocate(utf8MaxBytes); + + ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer); + byteBuf.clear(); + ByteBufUtil.writeUtf8(byteBuf, jsonStr); + buffer.limit(byteBuf.writerIndex()); + + return buffer; + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java new file mode 100644 index 0000000..b03db1d --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java @@ -0,0 +1,100 @@ +package com.njzscloud.common.redis.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.redis.annotation.RedisListener; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.*; + +/** + * Redis 监听器注册器, 暂未使用 + */ +@Slf4j +@Setter +public class RedisListenerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { + + private ResourceLoader resourceLoader; + + private Environment environment; + + @Override + public void registerBeanDefinitions(@NotNull AnnotationMetadata metadata, @NotNull BeanDefinitionRegistry registry) { + LinkedHashSet candidateComponents = new LinkedHashSet<>(); + + AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(RedisListener.class); + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.setResourceLoader(this.resourceLoader); + scanner.addIncludeFilter(annotationTypeFilter); + + Set basePackages = getBasePackages(metadata); + for (String basePackage : basePackages) { + basePackage = basePackage.trim(); + candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); + } + + for (BeanDefinition candidateComponent : candidateComponents) { + String beanClassName = candidateComponent.getBeanClassName(); + if (StrUtil.isBlank(beanClassName)) { + log.warn("当前组件不合法: [{}]", beanClassName); + continue; + } + if (registry.containsBeanDefinition(beanClassName)) { + log.debug("当前组件已存在: [{}]", beanClassName); + continue; + } + if (candidateComponent instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; + + AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); + if (annotationMetadata.isInterface() || annotationMetadata.isAbstract()) continue; + + String name = beanDefinition.getBeanClassName(); + registry.registerBeanDefinition(Objects.requireNonNull(name), beanDefinition); + } + } + } + + + protected Set getBasePackages(AnnotationMetadata importingClassMetadata) { + Map attributes = importingClassMetadata.getAnnotationAttributes(RedisListener.class.getCanonicalName()); + + Set basePackages = new HashSet<>(); + if (CollUtil.isNotEmpty(attributes)) { + for (String pkg : (String[]) attributes.get("value")) { + if (StringUtils.hasText(pkg)) { + basePackages.add(pkg); + } + } + } + + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); + } + return basePackages; + } + + protected ClassPathScanningCandidateComponentProvider getScanner() { + return new ClassPathScanningCandidateComponentProvider(false, this.environment) { + @Override + protected boolean isCandidateComponent(@NotNull AnnotatedBeanDefinition beanDefinition) { + return beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation(); + } + }; + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java new file mode 100644 index 0000000..23e4c07 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java @@ -0,0 +1,157 @@ +package com.njzscloud.common.redis.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import com.njzscloud.common.core.fastjson.Fastjson; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.redis.RedisCli; +import com.njzscloud.common.redis.annotation.RedisChannel; +import io.lettuce.core.pubsub.RedisPubSubListener; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * Redis 发布订阅模式,消息派发器 + */ +@Slf4j +public class RedisMessageDispatch implements RedisPubSubListener { + private final RedisCli redisCli; + + /** + * 频道/频道模式 —— 监听器对象,消息处理方法 + */ + Map>> dispatchList = new HashMap<>(); + + /** + * 构建消息派发器 + * + * @param redisCli Redis 客户端 {@link RedisCli} + */ + public RedisMessageDispatch(RedisCli redisCli) { + this.redisCli = redisCli; + this.redisCli.addListener(this); + } + + @Override + public void message(String channel, Object message) { + message(null, channel, message); + } + + @Override + public void message(String pattern, String channel, Object message) { + if (log.isDebugEnabled()) { + log.debug("收到 Redis 订阅消息,订阅模式:【{}】、订阅频道:【{}】、消息内容:【{}】", pattern, channel, Fastjson.toJsonStr(message)); + } + + List> methodList; + + if (pattern == null) methodList = dispatchList.get(channel); + else methodList = dispatchList.get(pattern); + + if (methodList == null || methodList.isEmpty()) return; + + for (Tuple2 tuple2 : methodList) { + Object target = tuple2.get_0(); + Method method = tuple2.get_1(); + + int parameterCount = method.getParameterCount(); + Object[] parameters = new Object[parameterCount]; + + if (parameterCount == 1) { + parameters[0] = message; + } else if (parameterCount == 2) { + parameters[0] = channel; + parameters[1] = message; + } else if (parameterCount == 3) { + parameters[0] = pattern; + parameters[1] = channel; + parameters[2] = message; + } + + CompletableFuture.runAsync(() -> { + try { + method.invoke(target, parameters); + } catch (Exception e) { + log.error("Redis 订阅消息处理失败,订阅模式:【{}】、订阅频道:【{}】、消息内容:【{}】", pattern, channel, Fastjson.toJsonStr(message), e); + } + }); + } + } + + @Override + public void subscribed(String channel, long count) { + log.info("订阅,频道:{}、订阅数:{}", channel, count); + } + + @Override + public void psubscribed(String pattern, long count) { + log.info("订阅,模式:{}、订阅数:{}", pattern, count); + } + + @Override + public void unsubscribed(String channel, long count) { + log.info("取消订阅,频道:{}、剩余订阅数:{}", channel, count); + } + + @Override + public void punsubscribed(String pattern, long count) { + log.info("取消订阅,模式:{}、剩余订阅数:{}", pattern, count); + } + + public void subscribe(Object listener) { + Class clazz = listener.getClass(); + HashSet exclude = CollUtil.newHashSet("wait", "equals", "toString", "hashCode", "getClass", "notify", "notifyAll"); + Method[] methods = ClassUtil.getPublicMethods(clazz, it -> { + int modifiers = it.getModifiers(); + int parameterCount = it.getParameterCount(); + String name = it.getName(); + return !exclude.contains(name) && !Modifier.isStatic(modifiers) && !Modifier.isAbstract(modifiers) && (parameterCount == 0 || parameterCount == 1 || parameterCount == 3); + }).toArray(new Method[0]); + if (methods.length == 0) return; + + Set registrationPatterns = new HashSet<>(); + Set registrationChannels = new HashSet<>(); + + String clazzName = clazz.getName(); + for (Method method : methods) { + RedisChannel redisChannel = method.getAnnotation(RedisChannel.class); + if (redisChannel == null) continue; + + Tuple2 listenerTuple = Tuple2.create(listener, method); + + registrationPatterns.addAll(resolveListener(listenerTuple, redisChannel.patterns(), dispatchList)); + + registrationChannels.addAll(resolveListener(listenerTuple, redisChannel.channels(), dispatchList)); + + if (log.isDebugEnabled()) { + log.debug("Redis 监听器,事件处理方法已注册:【{}#{}】", clazzName, method.getName()); + } + } + + this.redisCli.psubscribe(registrationPatterns); + this.redisCli.subscribe(registrationChannels); + } + + /** + * 计算要订阅的频道或频道模式 + */ + private Set resolveListener(Tuple2 listener, String[] channels, Map>> dispatchList) { + if (channels == null || channels.length == 0) return Collections.emptySet(); + + Set channelSet = Arrays.stream(channels).collect(Collectors.toSet()); + + Set registrations = new HashSet<>(channelSet); + + for (String channel : channelSet) { + List> listeners = dispatchList.computeIfAbsent(channel, k -> new ArrayList<>()); + listeners.add(listener); + } + + return registrations; + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java new file mode 100644 index 0000000..940f6c1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java @@ -0,0 +1,759 @@ +package com.njzscloud.common.redis.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.redis.RedisCli; +import io.lettuce.core.*; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.output.KeyStreamingChannel; +import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.output.ScoredValueStreamingChannel; +import io.lettuce.core.output.ValueStreamingChannel; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; + +/** + * Redis 工具 + */ +@Slf4j +@SuppressWarnings("unchecked") +public final class Redis { + + private final static RedisCli REDIS_CLI; + + static { + REDIS_CLI = SpringUtil.getBean(RedisCli.class); + } + + // region db/key + + /** + * 切换数据库 + * + * @param db 数据库索引 + * @return boolean + * @see RedisCommands#select(int) + */ + public static boolean select(int db) { + return REDIS_CLI.exec(it -> { + return "OK".equalsIgnoreCase(it.select(db)); + }); + } + + /** + * 向指定通道发送消息 + * + * @param channel 通道 + * @param message 消息 + * @return Long 接收消息的客户端数 + */ + public static Long publish(String channel, Object message) { + return REDIS_CLI.exec(it -> { + return it.publish(channel, message); + }); + } + + public static List keys(String pattern) { + return REDIS_CLI.exec(it -> { + return it.keys(pattern); + }); + } + + public static String rename(String key, String newKey) { + return REDIS_CLI.exec(it -> { + return it.rename(key, newKey); + }); + } + + public static Boolean renamenx(String key, String newKey) { + return REDIS_CLI.exec(it -> { + return it.renamenx(key, newKey); + }); + } + + public static Long del(String... keys) { + return REDIS_CLI.exec(it -> { + return it.del(keys); + }); + } + + public static Long exists(String... keys) { + return REDIS_CLI.exec(it -> { + return it.exists(keys); + }); + } + + public static Boolean expire(String key, long seconds) { + return REDIS_CLI.exec(it -> { + return it.expire(key, seconds); + }); + } + + public static Boolean expire(String key, Duration seconds) { + return REDIS_CLI.exec(it -> { + return it.expire(key, seconds); + }); + } + + public static Boolean expireat(String key, long timestamp) { + return REDIS_CLI.exec(it -> { + return it.expireat(key, timestamp); + }); + } + + public static Boolean expireat(String key, Date timestamp) { + return REDIS_CLI.exec(it -> { + return it.expireat(key, timestamp); + }); + } + + public static Boolean expireat(String key, Instant timestamp) { + return REDIS_CLI.exec(it -> { + return it.expireat(key, timestamp); + }); + } + // endregion + + // region str + + public static V get(String key) { + return REDIS_CLI.exec(it -> (V) it.get(key)); + } + + public static String set(String key, Object value, SetArgs setArgs) { + return REDIS_CLI.exec(it -> { + return it.set(key, value, setArgs); + }); + } + + public static String set(String key, Object value) { + return REDIS_CLI.exec(it -> { + return it.set(key, value); + }); + } + + public static String setex(String key, long seconds, Object value) { + return REDIS_CLI.exec(it -> { + return it.setex(key, seconds, value); + }); + } + + public static Boolean setnx(String key, Object value) { + return REDIS_CLI.exec(it -> { + return it.setnx(key, value); + }); + } + + public static List> mget(String... keys) { + return REDIS_CLI.exec(it -> { + return it.mget(keys); + }); + } + + public static Long mget(KeyValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.mget(channel, keys); + }); + } + + public static String mset(Map map) { + return REDIS_CLI.exec(it -> { + return it.mset(map); + }); + } + + public static Boolean msetnx(Map map) { + return REDIS_CLI.exec(it -> { + return it.msetnx(map); + }); + } + + public static Long incr(String key) { + return REDIS_CLI.exec(it -> { + return it.incr(key); + }); + } + + public static Long incrby(String key, long amount) { + return REDIS_CLI.exec(it -> { + return it.incrby(key, amount); + }); + } + + public static Long decr(String key) { + return REDIS_CLI.exec(it -> { + return it.decr(key); + }); + } + + public static Long decrby(String key, long amount) { + return REDIS_CLI.exec(it -> { + return it.decrby(key, amount); + }); + } + + public static Long strlen(String key) { + return REDIS_CLI.exec(it -> { + return it.strlen(key); + }); + } + + + public static Long bitcount(String key) { + return REDIS_CLI.exec(it -> { + return it.bitcount(key); + }); + } + + public static Long bitcount(String key, long start, long end) { + return REDIS_CLI.exec(it -> { + return it.bitcount(key, start, end); + }); + } + + public static List bitfield(String key, BitFieldArgs bitFieldArgs) { + return REDIS_CLI.exec(it -> { + return it.bitfield(key, bitFieldArgs); + }); + } + + public static Long bitpos(String key, boolean state) { + return REDIS_CLI.exec(it -> { + return it.bitpos(key, state); + }); + } + + public static Long bitpos(String key, boolean state, long start) { + return REDIS_CLI.exec(it -> { + return it.bitpos(key, state, start); + }); + } + + public static Long bitpos(String key, boolean state, long start, long end) { + return REDIS_CLI.exec(it -> { + return it.bitpos(key, state, start, end); + }); + } + + public static Long bitopAnd(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.bitopAnd(destination, keys); + }); + } + + public static Long bitopNot(String destination, String source) { + return REDIS_CLI.exec(it -> { + return it.bitopNot(destination, source); + }); + } + + public static Long bitopOr(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.bitopOr(destination, keys); + }); + } + + public static Long bitopXor(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.bitopXor(destination, keys); + }); + } + // endregion + + // region hash + public static Long hdel(String key, String... fields) { + return REDIS_CLI.exec(it -> { + return it.hdel(key, fields); + }); + } + + public static Boolean hexists(String key, String field) { + return REDIS_CLI.exec(it -> { + return it.hexists(key, field); + }); + } + + public static V hget(String key, String field) { + return REDIS_CLI.exec(it -> (V) it.hget(key, field)); + } + + public static Map hgetall(String key) { + return REDIS_CLI.exec(it -> { + return it.hgetall(key); + }); + } + + public static Long hgetall(KeyValueStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.hgetall(channel, key); + }); + } + + public static List hkeys(String key) { + return REDIS_CLI.exec(it -> { + return it.hkeys(key); + }); + } + + public static Long hkeys(KeyStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.hkeys(channel, key); + }); + } + + public static Long hlen(String key) { + return REDIS_CLI.exec(it -> { + return it.hlen(key); + }); + } + + public static List> hmget(String key, String... fields) { + return REDIS_CLI.exec(it -> { + return it.hmget(key, fields); + }); + } + + public static Long hmget(KeyValueStreamingChannel channel, String key, String... fields) { + return REDIS_CLI.exec(it -> { + return it.hmget(channel, key, fields); + }); + } + + public static String hmset(String key, Map map) { + return REDIS_CLI.exec(it -> { + return it.hmset(key, map); + }); + } + + public static Boolean hset(String key, String field, Object value) { + return REDIS_CLI.exec(it -> { + return it.hset(key, field, value); + }); + } + + public static Long hset(String key, Map map) { + return REDIS_CLI.exec(it -> { + return it.hset(key, map); + }); + } + + public static Boolean hsetnx(String key, String field, Object value) { + return REDIS_CLI.exec(it -> { + return it.hsetnx(key, field, value); + }); + } + + public static Long hstrlen(String key, String field) { + return REDIS_CLI.exec(it -> { + return it.hstrlen(key, field); + }); + } + + public static List hvals(String key) { + return REDIS_CLI.exec(it -> { + return it.hvals(key); + }); + } + + public static Long hvals(ValueStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.hvals(channel, key); + }); + } + // endregion + + // region list + public static V lindex(String key, long index) { + return REDIS_CLI.exec(it -> (V) it.lindex(key, index)); + } + + + public static Long llen(String key) { + return REDIS_CLI.exec(it -> { + return it.llen(key); + }); + } + + public static V lpop(String key) { + return REDIS_CLI.exec(it -> (V) it.lpop(key)); + } + + public static List lpop(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.lpop(key, count)); + } + + public static Long lpush(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.lpush(key, values); + }); + } + + public static Long lpushx(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.lpushx(key, values); + }); + } + + public static List lrange(String key, long start, long stop) { + return REDIS_CLI.exec(it -> (List) it.lrange(key, start, stop)); + } + + public static Long lrange(ValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.lrange((ValueStreamingChannel) channel, key, start, stop); + }); + } + + public static Long lrem(String key, long count, V value) { + return REDIS_CLI.exec(it -> { + return it.lrem(key, count, value); + }); + } + + public static String lset(String key, long index, V value) { + return REDIS_CLI.exec(it -> { + return it.lset(key, index, value); + }); + } + + public static String ltrim(String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.ltrim(key, start, stop); + }); + } + + public static V rpop(String key) { + return REDIS_CLI.exec(it -> (V) it.rpop(key)); + } + + public static List rpop(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.rpop(key, count)); + } + + public static Long rpush(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.rpush(key, values); + }); + } + + public static Long rpushx(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.rpushx(key, values); + }); + } + // endregion + + // region set + public static Long sadd(String key, V... members) { + return REDIS_CLI.exec(it -> { + return it.sadd(key, members); + }); + } + + public static Long scard(String key) { + return REDIS_CLI.exec(it -> { + return it.scard(key); + }); + } + + public static Set sdiff(String... keys) { + return REDIS_CLI.exec(it -> (Set) it.sdiff(keys)); + } + + public static Long sdiff(ValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sdiff((ValueStreamingChannel) channel, keys); + }); + } + + public static Long sdiffstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sdiffstore(destination, keys); + }); + } + + public static Set sinter(String... keys) { + return REDIS_CLI.exec(it -> (Set) it.sinter(keys)); + } + + public static Long sinter(ValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sinter((ValueStreamingChannel) channel, keys); + }); + } + + public static Long sinterstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sinterstore(destination, keys); + }); + } + + public static Boolean sismember(String key, V member) { + return REDIS_CLI.exec(it -> { + return it.sismember(key, member); + }); + } + + public static List smismember(String key, V... member) { + return REDIS_CLI.exec(it -> { + return it.smismember(key, member); + }); + } + + public static Set smembers(String key) { + return REDIS_CLI.exec(it -> (Set) it.smembers(key)); + } + + public static Long smembers(ValueStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.smembers((ValueStreamingChannel) channel, key); + }); + } + + public static V spop(String key) { + return REDIS_CLI.exec(it -> (V) it.spop(key)); + } + + public static Set spop(String key, long count) { + return REDIS_CLI.exec(it -> (Set) it.spop(key, count)); + } + + public static V srandmember(String key) { + return REDIS_CLI.exec(it -> (V) it.srandmember(key)); + } + + public static List srandmember(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.srandmember(key, count)); + } + + public static Long srandmember(ValueStreamingChannel channel, String key, long count) { + return REDIS_CLI.exec(it -> { + return it.srandmember((ValueStreamingChannel) channel, key, count); + }); + } + + public static Long srem(String key, V... members) { + return REDIS_CLI.exec(it -> { + return it.srem(key, members); + }); + } + + public static Set sunion(String... keys) { + return REDIS_CLI.exec(it -> (Set) it.sunion(keys)); + } + + public static Long sunion(ValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sunion((ValueStreamingChannel) channel, keys); + }); + } + + public static Long sunionstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sunionstore(destination, keys); + }); + } + + // endregion + + // region sorted set + public static Long zadd(String key, double score, V member) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, score, member); + }); + } + + public static Long zadd(String key, Object... scoresAndValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, scoresAndValues); + }); + } + + public static Long zadd(String key, ScoredValue... scoredValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, (ScoredValue[]) scoredValues); + }); + } + + public static Long zadd(String key, ZAddArgs zAddArgs, double score, V member) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, zAddArgs, score, member); + }); + } + + public static Long zadd(String key, ZAddArgs zAddArgs, Object... scoresAndValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, zAddArgs, scoresAndValues); + }); + } + + public static Long zadd(String key, ZAddArgs zAddArgs, ScoredValue... scoredValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, zAddArgs, (ScoredValue[]) scoredValues); + }); + } + + public static Long zcard(String key) { + return REDIS_CLI.exec(it -> { + return it.zcard(key); + }); + } + + public static Long zcount(String key, Range range) { + return REDIS_CLI.exec(it -> { + return it.zcount(key, range); + }); + } + + public static List zdiff(String... keys) { + return REDIS_CLI.exec(it -> (List) it.zdiff(keys)); + } + + public static Long zdiffstore(String destKey, String... srcKeys) { + return REDIS_CLI.exec(it -> { + return it.zdiffstore(destKey, srcKeys); + }); + } + + public static List zinter(String... keys) { + return REDIS_CLI.exec(it -> (List) it.zinter(keys)); + } + + public static List zinter(ZAggregateArgs aggregateArgs, String... keys) { + return REDIS_CLI.exec(it -> (List) it.zinter(aggregateArgs, keys)); + } + + public static Long zinterstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zinterstore(destination, keys); + }); + } + + public static Long zinterstore(String destination, ZStoreArgs storeArgs, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zinterstore(destination, storeArgs, keys); + }); + } + + public static V zrandmember(String key) { + return REDIS_CLI.exec(it -> (V) it.zrandmember(key)); + } + + public static List zrandmember(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.zrandmember(key, count)); + } + + public static ScoredValue zrandmemberWithScores(String key) { + return REDIS_CLI.exec(it -> (ScoredValue) it.zrandmemberWithScores(key)); + } + + public static List> zrandmemberWithScores(String key, long count) { + List> scoredValues = REDIS_CLI.exec(it -> (List>) it.zrandmemberWithScores(key, count)); + + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + + return list; + } + + public static List zrange(String key, long start, long stop) { + return REDIS_CLI.exec(it -> (List) it.zrange(key, start, stop)); + } + + public static Long zrange(ValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.zrange((ValueStreamingChannel) channel, key, start, stop); + }); + } + + public static List> zrangeWithScores(String key, long start, long stop) { + List> scoredValues = REDIS_CLI.exec(it -> (List>) it.zrangeWithScores(key, start, stop)); + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + + return list; + } + + public static Long zrangeWithScores(ScoredValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.zrangeWithScores((ScoredValueStreamingChannel) channel, key, start, stop); + }); + } + + public static List zrevrange(String key, long start, long stop) { + return REDIS_CLI.exec(it -> (List) it.zrevrange(key, start, stop)); + } + + public static Long zrevrange(ValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.zrevrange((ValueStreamingChannel) channel, key, start, stop); + }); + } + + public static List zunion(String... keys) { + return REDIS_CLI.exec(it -> (List) it.zunion(keys)); + } + + public static List zunion(ZAggregateArgs aggregateArgs, String... keys) { + return REDIS_CLI.exec(it -> (List) it.zunion(aggregateArgs, keys)); + } + + public static List> zunionWithScores(ZAggregateArgs aggregateArgs, String... keys) { + List> scoredValues = REDIS_CLI.exec(it -> (List>) it.zunionWithScores(aggregateArgs, keys)); + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + return list; + } + + public static List> zunionWithScores(String... keys) { + List> scoredValues = REDIS_CLI.exec(it -> (List>) it.zunionWithScores(keys)); + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + return list; + } + + public static Long zunionstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zunionstore(destination, keys); + }); + } + + public static Long zunionstore(String destination, ZStoreArgs storeArgs, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zunionstore(destination, storeArgs, keys); + }); + } + // endregion +} + diff --git a/njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..adb4e63 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.redis.config.RedisServiceAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-security/pom.xml b/njzscloud-common/njzscloud-common-security/pom.xml new file mode 100644 index 0000000..4bfc922 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-security + + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + + + + javax.servlet + javax.servlet-api + provided + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java new file mode 100644 index 0000000..2561f29 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java @@ -0,0 +1,161 @@ +package com.njzscloud.common.security.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.njzscloud.common.security.handler.AccessDeniedExceptionHandler; +import com.njzscloud.common.security.handler.AuthExceptionHandler; +import com.njzscloud.common.security.handler.LogoutPostHandler; +import com.njzscloud.common.security.module.password.PasswordAuthenticationProvider; +import com.njzscloud.common.security.module.password.PasswordLoginPreparer; +import com.njzscloud.common.security.permission.DefaultPermissionManager; +import com.njzscloud.common.security.permission.PermissionManager; +import com.njzscloud.common.security.permission.PermissionSecurityMetaDataSource; +import com.njzscloud.common.security.permission.PermissionVoter; +import com.njzscloud.common.security.support.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.vote.AffirmativeBased; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Configuration +@EnableConfigurationProperties({WebSecurityProperties.class}) +public class WebSecurityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(LoginHistoryRecorder.class) + public LoginHistoryRecorder loginHistoryRecorder() { + return new DefaultLoginHistoryRecorder(); + } + + @Bean + @ConditionalOnMissingBean(PermissionManager.class) + public PermissionManager permissionManager() { + return new DefaultPermissionManager(); + } + + @Bean + @ConditionalOnBean({IUserService.class, IRoleService.class, IResourceService.class}) + public PasswordAuthenticationProvider passwordAuthenticationProvider(IUserService iUserService, IRoleService iRoleService, IResourceService iResourceService) { + return new PasswordAuthenticationProvider(iUserService, iRoleService, iResourceService); + } + + @Bean + @ConditionalOnMissingBean({IUserService.class, IRoleService.class, IResourceService.class}) + public DefaultAuthenticationProvider defaultAuthenticationProvider() { + return new DefaultAuthenticationProvider(); + } + + @Bean + @ConditionalOnBean({IUserService.class, IRoleService.class, IResourceService.class}) + public PasswordLoginPreparer passwordLoginPreparer() { + return new PasswordLoginPreparer(); + } + + /** + * SpringSecurity 配置 + */ + @Slf4j + @Configuration + @RequiredArgsConstructor + public static class AuthenticationServerConfigurer { + + private final LoginHistoryRecorder loginHistoryRecorder; + + private final WebSecurityProperties webSecurityProperties; + + private final PermissionManager permissionManager; + + private final ObjectProvider loginPreparerObjectProvider; + + private final ObjectProvider abstractAuthenticationProviderObjectProvider; + + private FilterSecurityInterceptor createFilterSecurityInterceptor(HttpSecurity http, AuthenticationManager authenticationManager) { + FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor(); + securityInterceptor.setSecurityMetadataSource(new PermissionSecurityMetaDataSource(permissionManager)); + securityInterceptor.setAccessDecisionManager(new AffirmativeBased(Collections.singletonList(new PermissionVoter()))); + securityInterceptor.setAuthenticationManager(authenticationManager); + securityInterceptor.setRejectPublicInvocations(false); + securityInterceptor.setValidateConfigAttributes(false); + securityInterceptor.afterPropertiesSet(); + + http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); + return securityInterceptor; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + List loginPreparers = loginPreparerObjectProvider.orderedStream().collect(Collectors.toList()); + List authenticationProviders = abstractAuthenticationProviderObjectProvider.orderedStream().collect(Collectors.toList()); + ProviderManager providerManager = new ProviderManager(authenticationProviders); + + FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(http, providerManager); + + + LogoutPostHandler logoutPostHandler = new LogoutPostHandler(); + return http + + .csrf().disable() + .anonymous().disable() + .requestCache().disable() + .sessionManagement().disable() + + .securityContext() + .securityContextRepository(new TokenSecurityContextRepository()) + + .and() + .addFilter(securityInterceptor) + // 退出登录 + .logout() + + .addLogoutHandler(logoutPostHandler) + .logoutSuccessHandler(logoutPostHandler) + + .and() + .apply(new CombineAuthenticationConfigurer()) + .authenticationManager(providerManager) + .loginPreparers(loginPreparers) + .loginHistoryRecorder(loginHistoryRecorder) + + // 异常处理 + .and() + .exceptionHandling() + .authenticationEntryPoint(AuthExceptionHandler.INSTANCE) + .accessDeniedHandler(AccessDeniedExceptionHandler.INSTANCE) + + .and() + .build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> { + WebSecurity.IgnoredRequestConfigurer ignoring = web.ignoring(); + Set authIgnore = webSecurityProperties.getAuthIgnores(); + if (CollUtil.isNotEmpty(authIgnore)) { + ignoring.antMatchers(ArrayUtil.toArray(authIgnore, String.class)); + } + ignoring.antMatchers("/error"); + }; + } + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java new file mode 100644 index 0000000..b36d348 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.security.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; +import java.util.Set; + +@Getter +@Setter +@ConfigurationProperties(prefix = "spring.security") +public class WebSecurityProperties { + /** + * TOKEN 过期时间, ≤ 0-->永久,默认 0 + */ + private Duration tokenExp = Duration.ofMillis(0); + /** + * 不进行认证校验的路径, 按 Ant 格式匹配 + */ + private Set authIgnores = CollUtil.empty(Set.class); + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java new file mode 100644 index 0000000..bdce65e --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java @@ -0,0 +1,23 @@ +package com.njzscloud.common.security.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:auth_way + * 字典名称:登录方式 + */ +@Getter +@RequiredArgsConstructor +public enum AuthWay implements DictStr { + ANONYMOUS("Anonymous", "匿名登录"), + PASSWORD("Password", "账号密码登录"), + PHONE("Phone", "手机验证码登录"), + WECHAT("Wechat", "微信登录"), + WECHAT_MINI("WechatMini", "微信小程序登录"), + ; + + private final String val; + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java new file mode 100644 index 0000000..f5f544a --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.security.contant; + +import cn.hutool.core.collection.CollUtil; +import com.njzscloud.common.core.utils.Key; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserDetail; + +/** + * 常量 + */ +public final class Constants { + // 随机码字符串池 + public static final String RANDOM_BASE_STRING = "23456789abcdefjhjkmnpqrstuvwxyzABCDEFJHJKMNPQRSTUVWXYZ"; + + public static final String ROLE_AUTHENTICATED = "ROLE_AUTHENTICATED"; + public static final String ROLE_ANONYMOUS = "ROLE_ANONYMOUS"; + + // Redis 订阅频道 权限更新 + public static final String REDIS_TOPIC_PERMISSION_UPDATE = "permission_update"; + + public static final Key TOKEN_CACHE_KEY = Key.create("token:{userId}:{tid}"); + public static final String TOKEN_STR_SEPARATOR = ","; + + /** + * 匿名用户 + */ + public static final UserDetail ANONYMOUS_USER = new UserDetail() + .setUserId(0L) + .setAccountId(0L) + .setRoles(CollUtil.newHashSet(ROLE_ANONYMOUS)) + .setToken(Token.create(0L, 0L, AuthWay.ANONYMOUS)); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java new file mode 100644 index 0000000..e71e4c0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.security.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:endpoint_access_model + * 字典名称:接口访问模式 + */ +@Getter +@RequiredArgsConstructor +public enum EndpointAccessModel implements DictStr { + ANONYMOUS("Anonymous", "允许匿名访问"), + LOGINED("Logined", "允许已登录用户访问"), + AUTHENTICATED("Authenticated", "仅拥有权限的用户访问"), + FORBIDDEN("Forbidden", "禁止访问"), + ; + + private final String val; + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java new file mode 100644 index 0000000..be5875e --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.security.ex; + + +import org.springframework.security.access.AccessDeniedException; + +/** + * 禁止访问 + */ +public class ForbiddenAccessException extends AccessDeniedException { + + public ForbiddenAccessException(String msg) { + super(msg); + } + + public ForbiddenAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java new file mode 100644 index 0000000..fc0ba58 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.security.ex; + + +import org.springframework.security.access.AccessDeniedException; + +public class MissingPermissionException extends AccessDeniedException { + + public MissingPermissionException(String msg) { + super(msg); + } + + public MissingPermissionException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java new file mode 100644 index 0000000..da90a0c --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.security.ex; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import org.springframework.security.core.AuthenticationException; + +/** + * 认证异常 + */ +public class UserLoginException extends AuthenticationException { + + public final ExceptionMsg msg; + + public UserLoginException(ExceptionMsg exceptionMsg, String message) { + super(message); + this.msg = exceptionMsg; + } + + public UserLoginException(Throwable cause, ExceptionMsg exceptionMsg, String message, Object... param) { + super(StrUtil.format(message, param), cause); + this.msg = exceptionMsg; + } + + public UserLoginException(Throwable cause, ExceptionMsg exceptionMsg, String message) { + super(message, cause); + this.msg = exceptionMsg; + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java new file mode 100644 index 0000000..7222763 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java @@ -0,0 +1,52 @@ +package com.njzscloud.common.security.handler; + +import cn.hutool.extra.servlet.ServletUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.security.ex.MissingPermissionException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.AuthorizationServiceException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 权限异常 + */ +@Slf4j +public class AccessDeniedExceptionHandler implements AccessDeniedHandler { + + public static final AccessDeniedExceptionHandler INSTANCE = new AccessDeniedExceptionHandler(); + + private AccessDeniedExceptionHandler() { + } + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException, ServletException { + R r; + if (accessDeniedException instanceof AuthorizationServiceException) { + log.error("权限校验失败: {}", request.getRequestURI(), accessDeniedException); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "权限加载失败"); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } else if (accessDeniedException instanceof MissingPermissionException) { + log.error("权限未配置: {}", request.getRequestURI(), accessDeniedException); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "当前请求未分配权限"); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } else { + log.error("暂无权限: {}", request.getRequestURI(), accessDeniedException); + r = R.failed(ExceptionMsg.CLI_ERR_MSG, accessDeniedException.getMessage()); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java new file mode 100644 index 0000000..3fe45e7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.security.handler; + +import cn.hutool.extra.servlet.ServletUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + + +/** + * 认证异常 + */ +@Slf4j +public class AuthExceptionHandler implements AuthenticationEntryPoint { + public static final AuthExceptionHandler INSTANCE = new AuthExceptionHandler(); + + private AuthExceptionHandler() { + } + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException, ServletException { + log.error("未登录: {}", request.getRequestURI(), authException); + R r; + if (authException instanceof AuthenticationCredentialsNotFoundException) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + r = R.failed(ExceptionMsg.CLI_ERR_MSG, "登录凭证无效"); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "无法进行用户校验"); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java new file mode 100644 index 0000000..5c6c9b4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.security.handler; + +import cn.hutool.extra.servlet.ServletUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.support.AuthenticationDetails; +import com.njzscloud.common.security.support.LoginHistory; +import com.njzscloud.common.security.support.LoginHistoryRecorder; +import com.njzscloud.common.security.support.UserDetail; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.concurrent.CompletableFuture; + +/** + * 登陆后置处理器 + */ +@Slf4j +@RequiredArgsConstructor +public class LoginPostHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler { + + private final LoginHistoryRecorder loginHistoryRecorder; + + /** + * 登陆失败 + */ + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + log.error("登录失败", exception); + R r; + if (exception instanceof UserLoginException) { + r = R.failed(((UserLoginException) exception).msg, exception.getMessage()); + } else if (exception instanceof UsernameNotFoundException) { + r = R.failed(ExceptionMsg.CLI_ERR_MSG, "账号或密码错误"); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "无法进行用户校验"); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } + + /** + * 登陆成功 + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + R r = R.success(authentication.getPrincipal()); + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } + + /** + * 记录登陆日志 + * + * @param authentication 已认证的认证对象 + */ + public void recordLoginHistory(Authentication authentication) { + AuthenticationDetails details = (AuthenticationDetails) authentication.getDetails(); + UserDetail userDetail = (UserDetail) authentication.getPrincipal(); + AuthWay authWay = userDetail.getAuthWay(); + + long userAccountId = userDetail.getAccountId(); + long userId = userDetail.getUserId(); + LocalDateTime now = LocalDateTime.now(); + LoginHistory loginHistory = new LoginHistory() + .setUserId(userId) + .setAccountId(userAccountId) + .setLoginTime(now) + .setAuthWay(authWay) + .setIp(details.getRemoteAddress()) + .setUserAgent(details.getUserAgent()); + CompletableFuture.runAsync(() -> { + try { + loginHistoryRecorder.record(loginHistory); + } catch (Throwable e) { + log.error("登录日志记录失败", e); + } + }); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java new file mode 100644 index 0000000..8685958 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java @@ -0,0 +1,54 @@ +package com.njzscloud.common.security.handler; + +import cn.hutool.extra.servlet.ServletUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserAuthenticationToken; +import com.njzscloud.common.security.util.SecurityUtil; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 退出登陆处理器 + */ +public class LogoutPostHandler implements LogoutSuccessHandler, LogoutHandler { + + /** + * 退出登陆 + */ + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + UserAuthenticationToken userAuthenticationToken = (UserAuthenticationToken) authentication; + Token token = (Token) userAuthenticationToken.getCredentials(); + SecurityUtil.removeToken(token); + } + + /** + * 退出登陆成功 + */ + @Override + public void onLogoutSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + R r; + if (authentication == null) { + r = R.failed(Boolean.FALSE, ExceptionMsg.CLI_ERR_MSG, "未登陆,无需操作"); + } else { + r = R.success(Boolean.TRUE); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } + + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java new file mode 100644 index 0000000..fe851fa --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java @@ -0,0 +1,62 @@ +package com.njzscloud.common.security.module.password; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.support.*; +import com.njzscloud.common.security.util.EncryptUtil; +import lombok.RequiredArgsConstructor; + +import java.util.Set; + +/** + * 账号密码登录认证器 + */ +@RequiredArgsConstructor +public class PasswordAuthenticationProvider extends AbstractAuthenticationProvider { + private final IUserService iUserService; + private final IRoleService iRoleService; + private final IResourceService iResourceService; + + /** + * 读取用户信息 + * + * @param loginForm 登录表单 + * @return 用户信息 + */ + @Override + protected UserDetail retrieveUser(LoginForm loginForm) throws UserLoginException { + PasswordLoginForm passwordLoginForm = (PasswordLoginForm) loginForm; + String account = passwordLoginForm.getAccount(); + UserDetail userDetail = iUserService.selectUserByAccount(account); + if (userDetail == null) throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "账号或密码错误"); + Long userId = userDetail.getUserId(); + Set roles = iRoleService.selectRoleByUserId(userId); + Resource resource = iResourceService.selectResourceByUserId(userId); + return userDetail + .setAuthWay(AuthWay.PASSWORD) + .setRoles(roles) + .setResource(resource) + ; + } + + + @Override + protected void afterCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + String secret = userDetail.getSecret(); + PasswordLoginForm passwordLoginForm = (PasswordLoginForm) loginForm; + Assert.isTrue(EncryptUtil.matches(passwordLoginForm.getSecret(), secret), () -> new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "账号或密码错误")); + } + + /** + * 获取登录表单类型 + * + * @return 登录表单类型 + * @see PasswordLoginForm + */ + @Override + protected Class getLoginFormClazz() { + return PasswordLoginForm.class; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java new file mode 100644 index 0000000..d95ee0a --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java @@ -0,0 +1,42 @@ +package com.njzscloud.common.security.module.password; + +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.support.LoginForm; +import lombok.Getter; +import lombok.Setter; + +/** + * 登录参数 + */ +@Getter +@Setter +public class PasswordLoginForm extends LoginForm { + /** + * 登录账号 + */ + private String account; + + /** + * 登录密码 + */ + private String secret; + + /** + * 验证码 + */ + private String captcha; + + /** + * 获取验证码时使用的 uid + */ + private String captchaId; + + public PasswordLoginForm() { + super(AuthWay.PASSWORD); + } + + @Override + public String getName() { + return account; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java new file mode 100644 index 0000000..98c275f --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.security.module.password; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.support.LoginForm; +import com.njzscloud.common.security.support.LoginPreparer; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +/** + * 账号密码登录,登录参数处理器 + */ +public class PasswordLoginPreparer implements LoginPreparer { + private static final AntPathRequestMatcher matcher = new AntPathRequestMatcher("/login", "POST"); + + /** + * 创建登录参数 + * + * @param request 请求信息 + * @return 登录参数 {@link PasswordLoginForm} + */ + @Override + public LoginForm createLoginForm(HttpServletRequest request) { + try (ServletInputStream inputStream = request.getInputStream()) { + return Jackson.toBean(inputStream, PasswordLoginForm.class); + } catch (Exception e) { + throw new UserLoginException(e, ExceptionMsg.SYS_ERR_MSG, "登录表单解析失败"); + } + } + + /** + * 是否支持当前登录方式 + * + * @param request 请求信息 + * @return 当请求方式为 POST 且路径为 /login 时返回 true + */ + @Override + public boolean support(HttpServletRequest request) { + return matcher.matches(request); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java new file mode 100644 index 0000000..3e493b3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.security.permission; + +import com.njzscloud.common.security.contant.EndpointAccessModel; + +import java.util.Collections; +import java.util.List; + +/** + * 默认权限管理器
+ * 所有接口都必须登录后才能访问 + */ +public class DefaultPermissionManager extends PermissionManager { + + private final List DEFAULT_ROLE_PERMISSIONS = Collections.singletonList( + new RolePermission() + .setEndpoint("/**") + .setAccessModel(EndpointAccessModel.LOGINED) + ); + + @Override + protected List load() { + return DEFAULT_ROLE_PERMISSIONS; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java new file mode 100644 index 0000000..8356c3b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java @@ -0,0 +1,177 @@ +package com.njzscloud.common.security.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.contant.EndpointAccessModel; +import com.njzscloud.common.security.ex.ForbiddenAccessException; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +/** + * 权限管理器 + */ +@Slf4j +public abstract class PermissionManager { + + private static final ReentrantLock PERMISSION_CACHE_LOCK = new ReentrantLock(); + /** + * 权限缓存,请求地址——角色编码 + */ + private Map> PERMISSION_CACHE; + + private Set FORBIDDEN_CACHE; + + /** + * 刷新本地权限缓存 + */ + public final void refresh() { + try { + if (log.isDebugEnabled()) log.debug("刷新本地权限缓存已清除"); + PERMISSION_CACHE_LOCK.lock(); + PERMISSION_CACHE = null; + FORBIDDEN_CACHE = null; + if (log.isDebugEnabled()) log.debug("本地权限缓存已清除"); + this.load0(); + } finally { + PERMISSION_CACHE_LOCK.unlock(); + } + } + + /** + * 初始化本地权限缓存 + */ + public final void init() { + if (log.isDebugEnabled()) log.debug("初始化本地权限缓存"); + if (CollUtil.isEmpty(PERMISSION_CACHE)) { + try { + PERMISSION_CACHE_LOCK.lock(); + if (CollUtil.isEmpty(PERMISSION_CACHE)) { + this.load0(); + return; + } + } finally { + PERMISSION_CACHE_LOCK.unlock(); + } + } + if (log.isDebugEnabled()) log.debug("已初始化无需操作"); + } + + /** + * 加载权限 + */ + private void load0() { + if (log.isDebugEnabled()) log.debug("开始加载权限"); + + List rolePermissions = load(); + + if (rolePermissions == null) rolePermissions = Collections.emptyList(); + + Map> permissionMap = new LinkedHashMap<>(); + + Set forbiddenSet = new HashSet<>(); + + for (RolePermission rolePermission : rolePermissions) { + String endpoint = rolePermission.getEndpoint(); + String method = rolePermission.getMethod(); + EndpointAccessModel accessModel = rolePermission.getAccessModel(); + + AntPathRequestMatcher pathRequestMatcher = new AntPathRequestMatcher(endpoint, method); + if (accessModel == EndpointAccessModel.FORBIDDEN) { + forbiddenSet.add(pathRequestMatcher); + continue; + } + + Collection configAttributes = permissionMap.computeIfAbsent(pathRequestMatcher, it -> new HashSet<>()); + + if (accessModel == EndpointAccessModel.ANONYMOUS) { + configAttributes.add(new SecurityConfig(Constants.ROLE_ANONYMOUS)); + configAttributes.add(new SecurityConfig(Constants.ROLE_AUTHENTICATED)); + } else if (accessModel == EndpointAccessModel.LOGINED) { + configAttributes.add(new SecurityConfig(Constants.ROLE_AUTHENTICATED)); + } else if (accessModel == EndpointAccessModel.AUTHENTICATED) { + String role = rolePermission.getRole(); + if (StrUtil.isNotBlank(role)) configAttributes.add(new SecurityConfig(role)); + } + } + + FORBIDDEN_CACHE = forbiddenSet; + PERMISSION_CACHE = permissionMap; + + if (log.isDebugEnabled()) { + log.debug("本地权限缓存已加载:\n{}", Jackson.toJsonStr(this.getAllRelation())); + } + } + + /** + * 加载权限 + * + * @return List<RolePermission> + */ + abstract protected List load(); + + /** + * 获取当前请求所需要的角色 + * + * @param request 请求对象 + * @return Collection<ConfigAttribute> + */ + public final Collection extractAuthorities(HttpServletRequest request) { + this.init(); + if (FORBIDDEN_CACHE != null) { + for (AntPathRequestMatcher antPathRequestMatcher : FORBIDDEN_CACHE) { + if (antPathRequestMatcher.matches(request)) { + throw new ForbiddenAccessException("当前服务已停止使用!"); + } + } + } + if (PERMISSION_CACHE != null) { + for (Map.Entry> entry : PERMISSION_CACHE.entrySet()) { + if (entry.getKey().matches(request)) { + return entry.getValue(); + } + } + } + return CollUtil.empty(Set.class); + } + + /** + * 获取所有角色 + * + * @return 角色列表 + */ + public final Collection getAll() { + this.init(); + Set allAttributes = new HashSet<>(); + if (PERMISSION_CACHE != null) PERMISSION_CACHE.values().forEach(allAttributes::addAll); + return CollUtil.unmodifiable(allAttributes); + } + + /** + * 获取权限的字符表示形式 + * + * @return Map<String, Set<String>> + */ + public synchronized final Map> getAllRelation() { + Map> map = new HashMap<>(); + if (PERMISSION_CACHE != null) { + Set>> entries = PERMISSION_CACHE.entrySet(); + for (Map.Entry> entry : entries) { + AntPathRequestMatcher key = entry.getKey(); + Collection value = entry.getValue(); + Set collect = value.stream().map(ConfigAttribute::getAttribute).collect(Collectors.toSet()); + map.put(key.toString(), collect); + } + } + return MapUtil.unmodifiable(map); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java new file mode 100644 index 0000000..dece0ac --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java @@ -0,0 +1,48 @@ +package com.njzscloud.common.security.permission; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.security.ex.MissingPermissionException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; + +@Slf4j +@RequiredArgsConstructor +public class PermissionSecurityMetaDataSource implements FilterInvocationSecurityMetadataSource { + + // org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer + // org.springframework.security.access.vote.RoleVoter PermissionAuthorizationConfigurer + + private final PermissionManager permissionManager; + // private final boolean rejectPublicInvocations; + + + @Override + public Collection getAttributes(Object object) throws IllegalArgumentException { + HttpServletRequest request = ((FilterInvocation) object).getRequest(); + Collection permission = permissionManager.extractAuthorities(request); + String requestURI = request.getRequestURI(); + String method = request.getMethod(); + String endpoint = method.toUpperCase() + " " + requestURI; + + Assert.notEmpty(permission, () -> new MissingPermissionException(StrUtil.format("请求: 【{}】 未指定权限", endpoint))); + if (log.isDebugEnabled()) log.debug("允许访问接口:【{}】的角色:【{}】", endpoint, permission); + return permission; + } + + @Override + public Collection getAllConfigAttributes() { + return permissionManager.getAll(); + } + + @Override + public boolean supports(Class clazz) { + return FilterInvocation.class.isAssignableFrom(clazz); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java new file mode 100644 index 0000000..34dd614 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java @@ -0,0 +1,43 @@ +package com.njzscloud.common.security.permission; + +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * 投票器 + */ +public class PermissionVoter implements AccessDecisionVoter { + @Override + public int vote(Authentication authentication, Object object, Collection attributes) { + if (authentication == null) { + return ACCESS_DENIED; + } + int result = ACCESS_ABSTAIN; + Collection authorities = authentication.getAuthorities(); + for (ConfigAttribute attribute : attributes) { + if (this.supports(attribute)) { + result = ACCESS_DENIED; + for (GrantedAuthority authority : authorities) { + if (attribute.getAttribute().equals(authority.getAuthority())) { + return ACCESS_GRANTED; + } + } + } + } + return result; + } + + @Override + public boolean supports(ConfigAttribute attribute) { + return attribute.getAttribute() != null; + } + + @Override + public boolean supports(Class clazz) { + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java new file mode 100644 index 0000000..7f003f2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java @@ -0,0 +1,35 @@ +package com.njzscloud.common.security.permission; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.njzscloud.common.security.contant.EndpointAccessModel; + +/** + * 角色权限信息 + */ +@Getter +@Setter +@Accessors(chain = true) +public class RolePermission { + + /** + * 请求方法 + */ + private String method; + + /** + * 端点地址 + */ + private String endpoint; + + /** + * 接口访问模式 + */ + private EndpointAccessModel accessModel; + + /** + * 角色编码 + */ + private String role; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java new file mode 100644 index 0000000..c1fda6b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java @@ -0,0 +1,135 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.ex.UserLoginException; +import org.springframework.security.authentication.AccountStatusException; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Set; + +/** + * 认证器 + */ +public abstract class AbstractAuthenticationProvider implements AuthenticationProvider { + + /** + * 是否支持当前登录信息 + * + * @param authentication 认证信息类型(在 filter 中调用 authenticate 方法传入的待认证对象的类型) + * @return true-->支持 + */ + @Override + public final boolean supports(Class authentication) { + return UserAuthenticationToken.class.isAssignableFrom(authentication); + } + + @Override + public final Authentication authenticate(Authentication authentication) throws AuthenticationException { + UserAuthenticationToken userAuthenticationToken = (UserAuthenticationToken) authentication; + + Object principal = userAuthenticationToken.getPrincipal(); + + if (!principal.getClass().isAssignableFrom(this.getLoginFormClazz())) return null; + + LoginForm loginForm = (LoginForm) principal; + + this.beforeCheck(loginForm); + + UserDetail userDetail; + try { + userDetail = this.retrieveUser(loginForm); + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + throw new InternalAuthenticationServiceException("服务器异常,用户信息加载失败", e); + } + + Assert.notNull(userDetail, () -> new UsernameNotFoundException("用户不存在")); + + Set roles = userDetail.getRoles(); + if (CollUtil.isEmpty(roles)) { + userDetail.setRoles(CollUtil.newHashSet(Constants.ROLE_ANONYMOUS, Constants.ROLE_AUTHENTICATED)); + } else { + roles.add(Constants.ROLE_ANONYMOUS); + roles.add(Constants.ROLE_AUTHENTICATED); + } + + this.afterCheck(loginForm, userDetail); + + // 敏感信息擦除 + if (loginForm instanceof CredentialsContainer) { + ((CredentialsContainer) loginForm).eraseCredentials(); + } + + this.lastCheck(loginForm, userDetail); + AuthenticationDetails details = (AuthenticationDetails) userAuthenticationToken.getDetails(); + return this.createAuthentication(userDetail, details); + } + + /** + * 创建已完成认证的认证对象 + * + * @param userDetail 用户信息 + * @param details 额外登录信息 + * @return 已完成认证的认证对象 {@link UserAuthenticationToken} + */ + private Authentication createAuthentication(UserDetail userDetail, AuthenticationDetails details) { + Token token = Token.create(userDetail.getUserId(), userDetail.getAccountId(), userDetail.getAuthWay()); + userDetail.setToken(token); + return UserAuthenticationToken.create(userDetail, details); + } + + /** + * 表单检查,在查询用户信息之前执行 + * + * @param loginForm 登录表单 + * @throws UserLoginException 表单校验失败是抛出 + */ + protected void beforeCheck(LoginForm loginForm) throws UserLoginException { + } + + /** + * 用户信息检查(如:密码校验),在查询用户信息之后执行 + * + * @param userDetail 用户信息 + * @throws AccountStatusException 用户信息校验失败时抛出 + */ + protected void afterCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + } + + /** + * 最终检查,用于检查账号状态,如:是否锁定 + * + * @param userDetail 用户信息 + * @throws AccountStatusException 账号状态校验失败时抛出 + */ + protected void lastCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + if (userDetail.getDisabled() == Boolean.TRUE) { + throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "用户已被禁用"); + } + } + + /** + * 加载用户信息 + * + * @param loginForm 登录表单 + * @return 用户信息 + */ + protected abstract UserDetail retrieveUser(LoginForm loginForm) throws UserLoginException; + + /** + * 获取登录表单的类型 + * + * @return {@link LoginForm} 的子类 + */ + protected abstract Class getLoginFormClazz(); + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java new file mode 100644 index 0000000..9964a5c --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java @@ -0,0 +1,55 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.util.StrUtil; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.authentication.AuthenticationDetailsSource; + +import javax.servlet.http.HttpServletRequest; + +/** + *

用户认证详情

+ *

获取用户 IP, Nginx 反向代理 $remote_addr X-Real-IP

+ *

获取用户代理, Nginx 反向代理 $http_user_agent X-Real-UA

+ */ +@Getter +@Setter +@EqualsAndHashCode +public class AuthenticationDetails { + + /** + * 从请求对象中获取额外登录信息, IP 或 用户代理 + */ + private static final AuthenticationDetailsSource AUTHENTICATION_DETAILS_SOURCE = AuthenticationDetails::new; + + + /** + * 用户 IP + */ + private final String remoteAddress; + + /** + * 用户 Http 代理 + */ + private final String userAgent; + + public AuthenticationDetails(HttpServletRequest request) { + String x_real_ip = request.getHeader("X-Real-IP"); + if (StrUtil.isBlank(x_real_ip)) { + this.remoteAddress = request.getRemoteAddr(); + } else { + this.remoteAddress = x_real_ip; + } + String x_real_ua = request.getHeader("X-Real-UA"); + if (StrUtil.isBlank(x_real_ua)) { + this.userAgent = request.getHeader("UserDetail-Agent"); + } else { + this.userAgent = x_real_ua; + } + } + + public static AuthenticationDetails create(HttpServletRequest request) { + return AUTHENTICATION_DETAILS_SOURCE.buildDetails(request); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java new file mode 100644 index 0000000..65839cc --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java @@ -0,0 +1,83 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.handler.LoginPostHandler; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.context.SecurityContextRepository; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * 配置器 + */ +public class CombineAuthenticationConfigurer extends AbstractHttpConfigurer { + + /** + * 登陆预处理器 + */ + private final Collection loginPreparers = new ArrayList<>(); + /** + * 认证管理器 + */ + private AuthenticationManager authenticationManager; + /** + * 登陆后置处理器 + */ + private LoginPostHandler loginPostHandler; + + @Override + public void configure(HttpSecurity builder) throws Exception { + SecurityContextRepository securityContextRepository = builder.getSharedObject(SecurityContextRepository.class); + CombineAuthenticationFilter filter = new CombineAuthenticationFilter(loginPreparers, authenticationManager, loginPostHandler, securityContextRepository); + builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); + } + + /** + * 添加登陆预处理器 + * + * @param loginPreparer 登陆预处理器 + * @return 配置器对象 + */ + public CombineAuthenticationConfigurer addLoginPreparer(LoginPreparer loginPreparer) { + loginPreparers.add(loginPreparer); + return this; + } + + /** + * 添加登陆预处理器 + * + * @param loginPreparers 登陆预处理器 + * @return 配置器对象 + */ + public CombineAuthenticationConfigurer loginPreparers(Collection loginPreparers) { + this.loginPreparers.addAll(loginPreparers); + return this; + } + + /** + * 设置认证管理器 + * + * @param authenticationManager 认证管理器 + * @return 配置器对象 + * @see AbstractAuthenticationProvider + */ + public CombineAuthenticationConfigurer authenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + return this; + } + + /** + * 设置登录历史记录器 + * + * @param loginHistoryRecorder 登录历史记录器 + * @return 配置器对象 + * @see DefaultLoginHistoryRecorder + */ + public CombineAuthenticationConfigurer loginHistoryRecorder(LoginHistoryRecorder loginHistoryRecorder) { + this.loginPostHandler = new LoginPostHandler(loginHistoryRecorder); + return this; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java new file mode 100644 index 0000000..9f3c960 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java @@ -0,0 +1,143 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.handler.LoginPostHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.Optional; + +/** + * 认证过滤器 + */ +@Slf4j +@RequiredArgsConstructor +public class CombineAuthenticationFilter extends GenericFilterBean { + /** + * 支持的登录方式列表 + */ + private final Collection loginPreparers; + + /** + * 认证管理器{@link AbstractAuthenticationProvider} + */ + private final AuthenticationManager authenticationManager; + + /** + * 登陆后置处理器 + */ + private final LoginPostHandler loginPostHandler; + + /** + * TOKEN 存取器 + */ + private final SecurityContextRepository securityContextRepository; + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + Optional loginPreparerOptional = loginPreparers.stream().filter(it -> it.support(request)).findFirst(); + + if (!loginPreparerOptional.isPresent()) { + filterChain.doFilter(request, response); + return; + } + + LoginPreparer loginPreparer = loginPreparerOptional.get(); + + LoginForm loginForm; + try { + loginForm = loginPreparer.createLoginForm(request); + } catch (AuthenticationException failed) { + unsuccessfulAuthentication(request, response, failed); + return; + } catch (Exception e) { + unsuccessfulAuthentication(request, response, new InternalAuthenticationServiceException("登录表单解析失败", e)); + return; + } + + try { + Authentication authentication = createAuthenticationToken(request, loginForm); + // 开始认证 + Authentication authenticationResult = authenticationManager.authenticate(authentication); + // 不为空 则认证成功 + if (authenticationResult != null) { + successfulAuthentication(request, response, authenticationResult); + } + } catch (AuthenticationException failed) { + unsuccessfulAuthentication(request, response, failed); + } + } + + /** + * 构建待认证对象 + * + * @param request HTTP 请求对象 + * @return Authentication 待认证对象 + */ + private Authentication createAuthenticationToken(HttpServletRequest request, LoginForm loginForm) { + // 额外登录信息 + AuthenticationDetails details = AuthenticationDetails.create(request); + + return UserAuthenticationToken.create(loginForm, details); + } + + + /** + * 登录成功 + * + * @param request HTTP 请求对象 + * @param response HTTP 响应对象 + * @param authentication 已认证的认证对象 + */ + private void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + // 保存认证结果 + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + securityContextRepository.saveContext(context, request, response); + + loginPostHandler.recordLoginHistory(authentication); + + loginPostHandler.onAuthenticationSuccess(request, response, authentication); + + } + + /** + * 登录失败 + * + * @param request HTTP 请求对象 + * @param response HTTP 响应对象 + * @param failed 异常对象 + */ + private void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + // 清空认证信息 + SecurityContextHolder.clearContext(); + + // 认证失败后的处理 + loginPostHandler.onAuthenticationFailure(request, response, failed); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java new file mode 100644 index 0000000..439cb97 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.security.support; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +/** + * 认证器 + */ +public class DefaultAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return null; + } + + @Override + public boolean supports(Class authentication) { + return false; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java new file mode 100644 index 0000000..34f768a --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.security.support; + + +import com.njzscloud.common.core.jackson.Jackson; +import lombok.extern.slf4j.Slf4j; + +/** + * 默认登录历史记录器 + */ +@Slf4j +public class DefaultLoginHistoryRecorder implements LoginHistoryRecorder { + @Override + public void record(LoginHistory loginHistory) { + log.info("登陆成功:【{}】", Jackson.toJsonStr(loginHistory)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/EndpointResource.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/EndpointResource.java new file mode 100644 index 0000000..d7793fb --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/EndpointResource.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.security.support; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 端点信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class EndpointResource { + + /** + * Id + */ + private Long id; + + /** + * 请求方式; 字典代码:request_method + */ + private String requestMethod; + + /** + * 路由前缀; 以 / 开头 或 为空 + */ + private String routingPath; + + /** + * 端点地址; 以 / 开头, Ant 匹配模式 + */ + private String endpointPath; + + /** + * 接口访问模式; 字典代码:endpoint_access_model + */ + private String accessModel; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IResourceService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IResourceService.java new file mode 100644 index 0000000..e5252ae --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IResourceService.java @@ -0,0 +1,6 @@ +package com.njzscloud.common.security.support; + + +public interface IResourceService { + Resource selectResourceByUserId(Long userId); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IRoleService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IRoleService.java new file mode 100644 index 0000000..e23187d --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IRoleService.java @@ -0,0 +1,8 @@ +package com.njzscloud.common.security.support; + + +import java.util.Set; + +public interface IRoleService { + Set selectRoleByUserId(Long userId); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java new file mode 100644 index 0000000..3cf049d --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.security.support; + +public interface ITokenService { + + void saveToken(UserDetail userDetail); + + UserDetail loadUser(String tokenStr); + + void removeToken(Token token); + + void removeToken(Long userId); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IUserService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IUserService.java new file mode 100644 index 0000000..c77c487 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IUserService.java @@ -0,0 +1,5 @@ +package com.njzscloud.common.security.support; + +public interface IUserService { + UserDetail selectUserByAccount(String account); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java new file mode 100644 index 0000000..ea6d929 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java @@ -0,0 +1,20 @@ +package com.njzscloud.common.security.support; + + +import com.njzscloud.common.security.contant.AuthWay; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.security.Principal; + +@Getter +@Setter +@RequiredArgsConstructor +public abstract class LoginForm implements Principal { + /** + * 登录方式 + */ + private final AuthWay authWay; + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java new file mode 100644 index 0000000..7d6c414 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java @@ -0,0 +1,66 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.contant.AuthWay; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 登录记录 + */ +@Getter +@Setter +@Accessors(chain = true) +public class LoginHistory { + /** + * 用户 Id + */ + private Long userId; + + /** + * 账号 Id + */ + private Long accountId; + + /** + * 登录时间 + */ + private LocalDateTime loginTime; + + /** + * 认证方式 + */ + private AuthWay authWay; + + /** + * 用户代理 + */ + private String userAgent; + + /** + * 用户 IP + */ + private String ip; + + /** + * IP 归属地-省(代码) + */ + private Integer provinceCode; + + /** + * IP 归属地-省(名称) + */ + private String provinceName; + + /** + * IP 归属地-市(代码) + */ + private Integer cityCode; + + /** + * IP 归属地-市(名称) + */ + private String cityName; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java new file mode 100644 index 0000000..3c5313b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.security.support; + + +/** + * 登录历史记录器 + */ +public interface LoginHistoryRecorder { + void record(LoginHistory loginHistory); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java new file mode 100644 index 0000000..3cb2fe7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.security.support; + +import javax.servlet.http.HttpServletRequest; + +/** + * 登录前置处理器 + */ +public interface LoginPreparer { + /** + * 是否处理当前请求 + * + * @param request 请求对象 + * @return true-->处理器可以处理当前请求 + */ + boolean support(HttpServletRequest request); + + /** + * 创建登录表单 + * + * @param request 请求对象 + * @return 登录表单 + */ + LoginForm createLoginForm(HttpServletRequest request); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/MenuResource.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/MenuResource.java new file mode 100644 index 0000000..b663c4b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/MenuResource.java @@ -0,0 +1,69 @@ +package com.njzscloud.common.security.support; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + + +@Getter +@Setter +@Accessors(chain = true) +public class MenuResource { + /** + * 编号 + */ + private String sn; + + /** + * Id + */ + private Long id; + + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 层级 + */ + private Integer tier; + + /** + * 面包路径; 逗号分隔 + */ + private List breadcrumb; + + /** + * 类型; 字典代码:menu_category + */ + private String menuCategory; + + /** + * 标签是否冻结; 0-->否、1-->是 + */ + private Boolean freeze; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Resource.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Resource.java new file mode 100644 index 0000000..5a3379d --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Resource.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.security.support; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class Resource { + private List endpoint; + private List menu; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java new file mode 100644 index 0000000..774da51 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java @@ -0,0 +1,109 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.IdUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.alibaba.fastjson2.annotation.JSONField; +import com.njzscloud.common.core.fastjson.serializer.DictObjectDeserializer; +import com.njzscloud.common.core.fastjson.serializer.DictObjectSerializer; +import com.njzscloud.common.security.config.WebSecurityProperties; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.contant.Constants; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + + +/** + * Token + */ +@Getter +@Setter +@EqualsAndHashCode +@RequiredArgsConstructor +public final class Token { + /** + * 用户 Id + */ + private final long userId; + /** + * 账号 Id + */ + private final long accountId; + /** + * TOKEN Id + */ + private final String tid; + /** + * 签发时间(时间戳) + */ + private final long iat; + /** + * 过期时间(时间戳) + */ + private final long exp; + + /** + * 登录方式 + */ + @JSONField(serializeUsing = DictObjectSerializer.class, deserializeUsing = DictObjectDeserializer.class) + private final AuthWay authWay; + + /** + * 创建 TOKEN + * + * @param userId 用户 Id + * @param accountId 账号 Id + * @param authWay 登陆方式 + * @return Token 对象 + */ + public static Token create(long userId, long accountId, AuthWay authWay) { + WebSecurityProperties webSecurityProperties = SpringUtil.getBean(WebSecurityProperties.class); + long iat = System.currentTimeMillis(); + String tid = IdUtil.fastSimpleUUID(); + long tokenExp = webSecurityProperties.getTokenExp().getSeconds() * 1000; + long exp = tokenExp > 0 ? iat + tokenExp : 0; + return new Token(userId, accountId, tid, iat, exp, authWay); + } + + /** + * 创建 TOKEN + * + * @param token 字符串形式的 TOKEN + * @return Token 对象 + */ + public static Token create(String token) { + token = Base64.decodeStr(token); + String[] tokenSection = token.split(Constants.TOKEN_STR_SEPARATOR); + long userId = Long.parseLong(tokenSection[0]); + long accountId = Long.parseLong(tokenSection[1]); + String tid = tokenSection[2]; + long iat = Long.parseLong(tokenSection[3]); + long exp = Long.parseLong(tokenSection[4]); + AuthWay authWay = AuthWay.valueOf(tokenSection[5]); + return new Token(userId, accountId, tid, iat, exp, authWay); + } + + /** + * 字符串输出 + *

由 6 段组成,每段逗号分隔,再取 Base64

+ *

userId,accountId,tid,iat,exp,authWay(枚举名称)

+ */ + @Override + public String toString() { + return Base64.encode(userId + Constants.TOKEN_STR_SEPARATOR + + accountId + Constants.TOKEN_STR_SEPARATOR + + tid + Constants.TOKEN_STR_SEPARATOR + + iat + Constants.TOKEN_STR_SEPARATOR + + exp + Constants.TOKEN_STR_SEPARATOR + + authWay); + } + + /** + * 是否过期 + */ + public boolean isExpired() { + return exp > 0 && exp < System.currentTimeMillis() - 10 * 1000; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java new file mode 100644 index 0000000..6109d35 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java @@ -0,0 +1,125 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.util.SecurityUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * TOKEN 存取器 + */ +@Slf4j +public class TokenSecurityContextRepository implements SecurityContextRepository { + + /** + * 请求头 TOKEN 匹配正则 + */ + private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^(?[a-zA-Z0-9-.:_~+/]+=*)$", Pattern.CASE_INSENSITIVE); + + /** + * Websocket TOKEN 所在的请求头 + */ + private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + /** + * 解析 TOKEN + * + * @param requestResponseHolder 请求信息存储器 + * @return 认证信息 + */ + @Override + @SuppressWarnings("deprecation") + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + + HttpServletRequest request = requestResponseHolder.getRequest(); + // 普通请求头中的 TOKEN + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + + // 从 Query 参数中获取 TOKEN + if (StrUtil.isBlank(authorization)) { + String queryString = request.getQueryString(); + if (StrUtil.isNotBlank(queryString)) { + String[] split = queryString.split("&"); + for (String s : split) { + int idx = s.indexOf('='); + String key = idx > 0 ? URLUtil.decode(s.substring(0, idx), "UTF-8") : s; + if (HttpHeaders.AUTHORIZATION.equalsIgnoreCase(key)) { + authorization = idx > 0 && s.length() > idx + 1 ? URLUtil.decode(s.substring(idx + 1), "UTF-8") : null; + } + } + } + } + + // 从 Websocket 子协议请求头中获取 TOKEN + if (StrUtil.isBlank(authorization)) { + String websocketHeader = request.getHeader(SEC_WEB_SOCKET_PROTOCOL); + if (StrUtil.isNotBlank(websocketHeader)) { + authorization = websocketHeader.split(",")[0].trim(); + } + } + + UserDetail userDetail = null; + + if (StrUtil.isNotBlank(authorization)) { + authorization = URLUtil.decode(authorization); + Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization); + if (matcher.matches()) { + String tokenStr = matcher.group("token"); + try { + userDetail = SecurityUtil.parseToken(tokenStr); + } catch (Exception e) { + log.warn("TOKEN 解析失败", e); + } + } + } + + if (userDetail == null || userDetail.getToken().isExpired()) { + userDetail = Constants.ANONYMOUS_USER; + } + + log.info("当前登录人信息:{}", Jackson.toJsonStr(userDetail)); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + AuthenticationDetails details = AuthenticationDetails.create(request); + context.setAuthentication(UserAuthenticationToken.create(userDetail, details)); + return context; + } + + /** + * 保存 TOKEN + * + * @param context 认证信息 + * @param request 请求对象 + * @param response 响应对象 + */ + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = context.getAuthentication(); + // 退出登录时会为 null + if (!request.getRequestURI().startsWith("/login") || authentication == null) return; + UserAuthenticationToken userAuthenticationToken = (UserAuthenticationToken) authentication; + UserDetail userDetail = (UserDetail) userAuthenticationToken.getPrincipal(); + SecurityUtil.registrationToken(userDetail); + } + + /** + * 始终为 true + */ + @Override + public boolean containsContext(HttpServletRequest request) { + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java new file mode 100644 index 0000000..888c560 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.security.support; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * TOKEN 序列化器 + */ +public class TokenSerializer extends JsonSerializer { + @Override + public void serialize(Token value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(value.toString()); + } else { + gen.writeNull(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java new file mode 100644 index 0000000..433281d --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.security.support; + +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 待认证/已认证 令牌 + */ +@Getter +public class UserAuthenticationToken extends AbstractAuthenticationToken { + + /** + * 凭证 + */ + private final Object credentials; + + /** + * 认证主体,未认证时为:{@link LoginForm},认证完成时为:{@link UserDetail} + */ + private final Object principal; + + /** + * 创建认证对象 + * + * @param credentials 凭证 + * @param principal 认证主体,未认证时为:{@link LoginForm},认证完成时为:{@link UserDetail} + * @param authorities 权限信息(角色编码) + * @param details 登录附加信息{@link AuthenticationDetails} + * @param authenticated 是否已完成认证 + */ + private UserAuthenticationToken(Object credentials, + Object principal, + Collection authorities, + AuthenticationDetails details, + boolean authenticated) { + super(authorities); + setAuthenticated(authenticated); + setDetails(details); + this.credentials = credentials; + this.principal = principal; + } + + /** + * 创建未认证的认证对象 + * + * @param principal 认证主体,未认证时为:{@link LoginForm} + * @param details 登录附加信息{@link AuthenticationDetails} + * @return 待认证对象 + */ + public static UserAuthenticationToken create(LoginForm principal, AuthenticationDetails details) { + return new UserAuthenticationToken(null, principal, null, details, false); + } + + /** + * 创建已完成认证的认证对象 + * + * @param principal 用户信息{@link UserDetail} + * @return 已完成认证的认证对象 + */ + public static UserAuthenticationToken create(UserDetail principal, AuthenticationDetails details) { + Token credentials = principal.getToken(); + Set roles = principal.getRoles(); + Set authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet()); + return new UserAuthenticationToken(credentials, principal, authorities, details, true); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java new file mode 100644 index 0000000..1fe48ba --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java @@ -0,0 +1,110 @@ +package com.njzscloud.common.security.support; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.njzscloud.common.core.fastjson.Fastjson; +import com.njzscloud.common.core.fastjson.serializer.DictObjectDeserializer; +import com.njzscloud.common.core.fastjson.serializer.DictObjectSerializer; +import com.njzscloud.common.security.contant.AuthWay; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.security.core.CredentialsContainer; + +import java.security.Principal; +import java.util.Set; + +/** + * 用户信息 + */ +@Getter +@Setter +@Accessors(chain = true) +public class UserDetail implements CredentialsContainer, Principal { + + /** + * 用户 Id + */ + private Long userId; + + /** + * 昵称 + */ + private String nickname; + + /** + * 密码 + */ + private String secret; + + /** + * 账号 Id + */ + private Long accountId; + + private Long tenantId; + + private String tenantName; + /** + * 业务对象 + */ + private String bizObj; + + /** + * 登录方式 + */ + @JSONField(serializeUsing = DictObjectSerializer.class, deserializeUsing = DictObjectDeserializer.class) + private AuthWay authWay; + + /** + * 角色编码 + */ + private Set roles; + + /** + * 资源 + */ + private Resource resource; + + @JsonSerialize(using = TokenSerializer.class) + private Token token; + + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + /** + * 账号是否过期 + */ + // private boolean accountExpired = false; + + /** + * 账号是否被锁定 + */ + // private boolean accountLocked = false; + + /** + * 密码是否过期 + */ + // private boolean credentialsExpired = false; + + /** + * 是否启用 + */ + // private boolean disable = false; + @Override + public String toString() { + return Fastjson.toJsonStr(this); + } + + @Override + public String getName() { + return userId.toString(); + } + + @Override + public void eraseCredentials() { + this.secret = null; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java new file mode 100644 index 0000000..97a9d5f --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java @@ -0,0 +1,64 @@ +package com.njzscloud.common.security.util; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.security.contant.Constants; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * 加密工具 + */ +public class EncryptUtil { + /** + * 加密器 + */ + private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder(); + + /** + * 加密 + * + * @param password 密码(明文) + * @return 密码(密文) + */ + public static String encrypt(String password) { + if (StrUtil.isBlank(password)) return null; + return ENCODER.encode(password); + } + + /** + * 匹配密码 + * + * @param rawPassword 密码(明文) + * @param encodedPassword 密码(密文) + * @return 匹配结果, true: 成功 + */ + public static boolean matches(String rawPassword, String encodedPassword) { + return ENCODER.matches(rawPassword, encodedPassword); + } + + /** + * 获取随机密码, 默认长度为 6 + * + * @return String[ ] 0-->密码(原文), 1-->密码(密文) + */ + public static String[] randomPassword() { + return randomPassword(6); + } + + /** + * 获取随机密码 + * + * @param len 密码长度 + * @return String[ ] 0-->密码(原文), 1-->密码(密文) + */ + public static String[] randomPassword(int len) { + String password = RandomUtil.randomString(Constants.RANDOM_BASE_STRING, len); + String encryptedPassword = encrypt(password); + return new String[]{password, encryptedPassword}; + } + + public static void main(String[] args) { + System.out.println(encrypt("admin")); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java new file mode 100644 index 0000000..0c35d8e --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java @@ -0,0 +1,61 @@ +package com.njzscloud.common.security.util; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.support.ITokenService; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserDetail; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * 获取认证信息工具 + */ +public class SecurityUtil { + /** + * 获取当前登录主体信息 + * + * @return UserAuthPrincipal + */ + public static UserDetail loginUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserDetail userDetail = null; + if (authentication != null) userDetail = (UserDetail) authentication.getPrincipal(); + return userDetail == null ? Constants.ANONYMOUS_USER : userDetail; + } + + /** + * 保存用户信息和 TOKEN + * + * @param userDetail 用户信息 + */ + public static void registrationToken(UserDetail userDetail) { + SpringUtil.getBean(ITokenService.class).saveToken(userDetail); + } + + /** + * 解析 TOKEN + * + * @param tokenStr TOKEN 字符串 + * @return Token + * @see Token + */ + public static UserDetail parseToken(String tokenStr) { + return SpringUtil.getBean(ITokenService.class).loadUser(tokenStr); + } + + /** + * 解析 TOKEN + * + * @param token TOKEN + * @see Token + */ + public static void removeToken(Token token) { + SpringUtil.getBean(ITokenService.class).removeToken(token); + } + + + public static void removeToken(Long userId) { + SpringUtil.getBean(ITokenService.class).removeToken(userId); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a331187 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.security.config.WebSecurityAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-sichen/pom.xml b/njzscloud-common/njzscloud-common-sichen/pom.xml new file mode 100644 index 0000000..863375f --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/pom.xml @@ -0,0 +1,38 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-sichen + jar + + sichen + http://maven.apache.org + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-mp + provided + + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskEntity.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskEntity.java new file mode 100644 index 0000000..3067255 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskEntity.java @@ -0,0 +1,65 @@ +package com.njzscloud.common.sichen; + +import com.njzscloud.common.sichen.contant.ScheduleType; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * 定时任务表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_task") +public class SysTaskEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 任务名称 + */ + private String taskName; + + /** + * 任务执行函数 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private String scheduleConf; + + /** + * 临界时间 + */ + private Long criticalTiming; + + /** + * 是否禁用; 0-->否、1-->是 + */ + private Boolean disabled; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskMapper.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskMapper.java new file mode 100644 index 0000000..9e4dd0b --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.sichen; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 定时任务表 + */ +@Mapper +public interface SysTaskMapper extends BaseMapper { + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskService.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskService.java new file mode 100644 index 0000000..1ceb220 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/SysTaskService.java @@ -0,0 +1,73 @@ +package com.njzscloud.common.sichen; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 定时任务表 + */ +@Slf4j +public class SysTaskService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysTaskEntity 数据 + */ + public void add(SysTaskEntity sysTaskEntity) { + TaskInfo taskInfo = BeanUtil.copyProperties(sysTaskEntity, TaskInfo.class) + .setCriticalTiming(TaskUtil.computedNextTiming(sysTaskEntity.getScheduleType(), sysTaskEntity.getScheduleConf())); + this.save(sysTaskEntity.setCriticalTiming(taskInfo.getCriticalTiming())); + } + + /** + * 修改 + * + * @param sysTaskEntity 数据 + */ + public void modify(SysTaskEntity sysTaskEntity) { + this.updateById(sysTaskEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysTaskEntity 结果 + */ + public SysTaskEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysTaskEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysTaskEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, SysTaskEntity sysTaskEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysTaskEntity))); + } + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java new file mode 100644 index 0000000..4ddfbd2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java @@ -0,0 +1,50 @@ +package com.njzscloud.common.sichen.config; + +import com.njzscloud.common.core.thread.ThreadPool; +import com.njzscloud.common.sichen.SysTaskService; +import com.njzscloud.common.sichen.dispatcher.SichenScheduler; +import com.njzscloud.common.sichen.executor.SichenExecutor; +import com.njzscloud.common.sichen.support.TaskStore; +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@MapperScan("com.njzscloud.common.sichen") +@EnableConfigurationProperties(TaskProperties.class) +public class TaskAutoConfiguration { + + @Bean + public SysTaskService sysTaskService() { + return new SysTaskService(); + } + + @Bean + public TaskStore taskStore(SysTaskService sysTaskService) { + return new TaskStore(sysTaskService); + } + + @Bean(destroyMethod = "stop") + public SichenExecutor sichenExecutor(TaskProperties taskProperties) { + TaskProperties.Pool pool = taskProperties.getPool(); + return new SichenExecutor(ThreadPool.createThreadPool( + pool.getPoolName(), + pool.getCorePoolSize(), + pool.getMaxPoolSize(), + pool.getKeepAliveSeconds(), + pool.getWindowCapacity(), + pool.getStandbyCapacity(), + (r, p) -> log.error("任务执行器线程池已满,拒绝任务:{}", r.toString()) + )); + } + + @Bean(destroyMethod = "stop") + public SichenScheduler sichenScheduler(TaskStore taskStore) { + return new SichenScheduler(taskStore); + } + + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java new file mode 100644 index 0000000..5f7eff3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java @@ -0,0 +1,27 @@ +package com.njzscloud.common.sichen.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@ConfigurationProperties(prefix = "task") +public class TaskProperties { + + private Pool pool = new Pool(); + + @Getter + @Setter + @ToString + public static class Pool { + private String poolName = "任务执行器"; + private int corePoolSize = 10; + private int maxPoolSize = 200; + private long keepAliveSeconds = 300; + private int windowCapacity = 20; + private int standbyCapacity = 8192; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java new file mode 100644 index 0000000..3e9d462 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.sichen.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ScheduleType implements DictStr { + Manually("Manually", "手动"), + Fixed("Fixed", "固定周期"), + Cron("Cron", "自定义"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java new file mode 100644 index 0000000..f7f690e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java @@ -0,0 +1,20 @@ +package com.njzscloud.common.sichen.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TaskStatus implements DictStr { + Waiting("Waiting", "等待调度"), + Pending("Pending", "排队中"), + Running("Running", "运行中"), + Completed("Completed", "已完成"), + Error("Error", "错误"); + + private final String val; + + private final String txt; +} + diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/dispatcher/SichenScheduler.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/dispatcher/SichenScheduler.java new file mode 100644 index 0000000..3542608 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/dispatcher/SichenScheduler.java @@ -0,0 +1,130 @@ +package com.njzscloud.common.sichen.dispatcher; + +import cn.hutool.core.thread.ThreadUtil; +import com.njzscloud.common.sichen.support.TaskHandle; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskUtil; +import com.njzscloud.common.sichen.support.Cable; +import com.njzscloud.common.sichen.support.TaskStore; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class SichenScheduler { + + private static final int SCAN_TASK_PERIOD = 5000; + private final Map> todolist = new ConcurrentHashMap<>(); + private final TaskStore taskStore; + private final Thread scanThread; + private final Thread dripThread; + + public SichenScheduler(TaskStore taskStore) { + log.info("任务调度器启动中..."); + this.taskStore = taskStore; + scanThread = new Thread(this::scanTask); + scanThread.setName("任务扫描线程"); + scanThread.start(); + dripThread = new Thread(this::drip); + dripThread.setName("计时器线程"); + dripThread.start(); + log.info("任务调度器已启动"); + } + + public void stop() { + log.info("正在停止任务调度器..."); + ThreadUtil.interrupt(scanThread, true); + ThreadUtil.interrupt(dripThread, true); + log.info("任务调度器已停止"); + } + + private void scanTask() { + long cost = 0; + while (true) { + if (cost < SCAN_TASK_PERIOD) { + try { + TimeUnit.MILLISECONDS.sleep(SCAN_TASK_PERIOD - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + log.info("任务扫描线程被中断"); + return; + } + } + long now = System.currentTimeMillis(); + long soon = now + SCAN_TASK_PERIOD; + + try { + List taskInfos = taskStore.load(now / 1000, soon / 1000); + if (taskInfos != null && !taskInfos.isEmpty()) { + schedule(now / 1000, soon / 1000, taskInfos); + } + } catch (Exception e) { + log.error("任务调度失败", e); + } + + cost = System.currentTimeMillis() - now; + } + } + + private void schedule(long now, long soon, List taskInfos) { + try { + List updateList = new LinkedList<>(); + for (TaskInfo taskInfo : taskInfos) { + try { + long timing = taskInfo.getCriticalTiming(); + if (timing == now) { + Cable.execute(new TaskHandle(taskInfo)); + } else if (timing > now) { + List tasks = todolist.computeIfAbsent(timing % 60, k -> new LinkedList<>()); + tasks.add(new TaskHandle(taskInfo)); + } + long criticalTiming = timing; + for (int i = 0; ; i++) { + timing = TaskUtil.computedNextTiming(now + i, taskInfo); + if (timing != 0 && timing > now && timing < soon) { + if (criticalTiming == timing) { + continue; + } + List tasks = todolist.computeIfAbsent(timing % 60, k -> new LinkedList<>()); + tasks.add(new TaskHandle(taskInfo)); + criticalTiming = timing; + } else { + criticalTiming = timing; + break; + } + } + updateList.add(new TaskInfo(taskInfo).setCriticalTiming(criticalTiming)); + } catch (Exception e) { + log.error("任务调度失败", e); + } + } + taskStore.update(updateList); + } catch (Exception e) { + log.error("任务调度失败", e); + } + } + + private void drip() { + while (true) { + try { + TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + log.info("计时器线程被中断"); + return; + } + + long now = System.currentTimeMillis() / 1000; + + for (int i = 0; i < 2; i++) { + List tasks = todolist.remove(now % 60 + i); + if (tasks == null || tasks.isEmpty()) { + continue; + } + Cable.execute(tasks); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java new file mode 100644 index 0000000..2f8ab5e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java @@ -0,0 +1,43 @@ +package com.njzscloud.common.sichen.executor; + +import com.njzscloud.common.core.thread.ThreadPool; +import com.njzscloud.common.sichen.support.TaskHandle; +import com.njzscloud.common.sichen.contant.TaskStatus; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ThreadPoolExecutor; + +@Slf4j +public class SichenExecutor { + + private final ThreadPoolExecutor taskThreadPool; + + public SichenExecutor(ThreadPoolExecutor taskThreadPool) { + log.info("任务执行器启动中..."); + this.taskThreadPool = taskThreadPool; + log.info("任务执行器已启动"); + } + + public SichenExecutor() { + this(ThreadPool.createThreadPool("任务执行器", + 10, 200, + 300, + 20, 8192, + (r, p) -> log.error("任务执行器线程池已满,拒绝任务:{}", r.toString()))); + } + + public void execute(TaskHandle taskHandle) { + if (taskHandle.getStatus() != TaskStatus.Waiting) { + return; + } + taskHandle.setStatus(TaskStatus.Pending); + taskThreadPool.execute(taskHandle); + } + + public void stop() { + log.info("正在停止任务执行器..."); + taskThreadPool.shutdownNow(); + log.info("任务执行器已停止"); + } + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Cable.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Cable.java new file mode 100644 index 0000000..585e8ec --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Cable.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.sichen.support; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.sichen.dispatcher.SichenScheduler; +import com.njzscloud.common.sichen.executor.SichenExecutor; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; +import java.util.List; + +@Slf4j +public class Cable { + private final static SichenScheduler SICHEN_SCHEDULER = SpringUtil.getBean(SichenScheduler.class); + private final static SichenExecutor SICHEN_EXECUTOR = SpringUtil.getBean(SichenExecutor.class); + private final static TaskStore taskStore = SpringUtil.getBean(TaskStore.class); + + public static Tuple2 getFn(String fn) { + return taskStore.getFn(fn); + } + + public static void execute(TaskHandle taskHandle) { + try { + SICHEN_EXECUTOR.execute(taskHandle); + } catch (Exception e) { + log.error("任务添加失败", e); + } + } + + public static void execute(List tasks) { + if (tasks == null || tasks.isEmpty()) { + return; + } + for (TaskHandle taskHandle : tasks) { + try { + SICHEN_EXECUTOR.execute(taskHandle); + } catch (Exception e) { + log.error("任务添加失败", e); + } + } + } + + public static void report(TaskHandle taskHandle) { + } + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java new file mode 100644 index 0000000..f1ede40 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java @@ -0,0 +1,1670 @@ +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package com.njzscloud.common.sichen.support; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.*; + +/** + * 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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
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.3.1 + */ +public final class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map 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 months; + protected transient TreeSet daysOfWeek; + protected transient TreeSet years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws 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(); + } + 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 (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); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet 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.toString() + ")", 0); + } + } + + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if (type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if (c == '-') { + ValueSet vs = getValue(0, s, i + 1); + lastdayOffset = vs.value; + if (lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i + 1); + i = vs.pos; + } + if (s.length() > i) { + c = s.charAt(i); + if (c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if (val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if (val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.Set 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.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if (!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if (t > day) { + mon++; + if (mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if (dow == Calendar.SATURDAY) { + day -= 1; + } else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if (dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if (nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if (dow == Calendar.SATURDAY) { + day -= 1; + } else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if (dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(Calendar.HOUR_OF_DAY, hour); + if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(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 void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java new file mode 100644 index 0000000..2708ab0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.sichen.support; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; + +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Task { + String value() default ""; + + String memo() default ""; + + String cron() default ""; + + int period() default 0; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java new file mode 100644 index 0000000..d8683fc --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.sichen.support; + +import cn.hutool.core.util.IdUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.sichen.contant.TaskStatus; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; + +@Getter +@Setter +@Slf4j +@Accessors(chain = true) +public class TaskHandle implements Runnable { + private Long taskId; + private Long scheduleId; + private String memo; + + private String fname; + private Tuple2 fn; + private TaskStatus status; + private Long startTime; + private Long endTime; + + public TaskHandle(TaskInfo taskInfo) { + this.taskId = taskInfo.getId(); + this.scheduleId = IdUtil.getSnowflakeNextId(); + this.memo = taskInfo.getMemo(); + this.status = TaskStatus.Waiting; + this.fname = taskInfo.getFn(); + this.fn = Cable.getFn(this.fname); + } + + @Override + public void run() { + if (status != TaskStatus.Pending) { + return; + } + status = TaskStatus.Running; + startTime = System.currentTimeMillis(); + try { + if (fn == null) { + status = TaskStatus.Error; + log.error("任务执行失败:{},任务处理函数不存在:{}", memo, fname); + } + fn.get_1().invoke(fn.get_0()); + status = TaskStatus.Completed; + } catch (Throwable e) { + status = TaskStatus.Error; + log.error("任务执行失败:{}", memo, e); + } finally { + endTime = System.currentTimeMillis(); + Cable.report(this); + } + } + + @Override + public String toString() { + return "TaskHandle{" + + "taskId=" + taskId + + ", scheduleId=" + scheduleId + + ", taskName='" + memo + '\'' + + ", fname='" + fname + '\'' + + ", status=" + status + + ", startTime=" + startTime + + ", endTime=" + endTime + + '}'; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java new file mode 100644 index 0000000..aa0ecf8 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java @@ -0,0 +1,48 @@ +package com.njzscloud.common.sichen.support; + +import cn.hutool.core.date.DateUtil; +import com.njzscloud.common.sichen.contant.ScheduleType; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; + +@Setter +@Getter +@Slf4j +@Accessors(chain = true) +public class TaskInfo { + private Long id; + private String memo; + private String fn; + private ScheduleType scheduleType; + private String scheduleConf; + private Long criticalTiming; + + + public TaskInfo() { + } + + public TaskInfo(TaskInfo taskInfo) { + id = taskInfo.id; + memo = taskInfo.memo; + fn = taskInfo.fn; + scheduleType = taskInfo.scheduleType; + scheduleConf = taskInfo.scheduleConf; + criticalTiming = taskInfo.criticalTiming; + } + + @Override + public String toString() { + return "TaskInfo{" + + "id=" + id + + ", memo='" + memo + '\'' + + ", fn='" + fn + '\'' + + ", scheduleType=" + scheduleType + + ", scheduleConf='" + scheduleConf + '\'' + + ", criticalTiming=" + DateUtil.formatDateTime(new Date(criticalTiming * 1000)) + + '}'; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java new file mode 100644 index 0000000..9d5f44c --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java @@ -0,0 +1,106 @@ +package com.njzscloud.common.sichen.support; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.GroupUtil; +import com.njzscloud.common.sichen.*; +import com.njzscloud.common.sichen.contant.ScheduleType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +public class TaskStore implements BeanPostProcessor { + private final Map> fn = new HashMap<>(); + private final List tasks = new LinkedList<>(); + private final SysTaskService sysTaskService; + + + public Tuple2 getFn(String fname) { + return fn.get(fname); + } + + public List load(long now, long soon) { + // log.info("加载任务:{}", DateUtil.formatDateTime(new Date(soon * 1000))); + List l1 = sysTaskService.list(Wrappers.lambdaQuery() + .lt(SysTaskEntity::getCriticalTiming, soon) + .eq(SysTaskEntity::getDisabled, false) + ) + .stream() + .map(it -> BeanUtil.copyProperties(it, TaskInfo.class)) + .collect(Collectors.toList()); + List l2 = tasks.stream() + .filter(it -> it.getCriticalTiming() <= soon) + .collect(Collectors.toList()); + l2.addAll(l1); + return l2; + } + + public void update(List tasks) { + if (tasks == null || tasks.isEmpty()) return; + Map map = GroupUtil.k_o(tasks, TaskInfo::getId); + + ArrayList list = new ArrayList<>(); + for (TaskInfo task : this.tasks) { + TaskInfo taskInfo = map.get(task.getId()); + if (taskInfo == null) { + list.add(new SysTaskEntity() + .setId(task.getId()) + .setCriticalTiming(task.getCriticalTiming()) + .setDisabled(task.getCriticalTiming() <= 0 ? true : null) + ); + continue; + } + task.setCriticalTiming(taskInfo.getCriticalTiming()); + } + if (list.isEmpty()) return; + sysTaskService.updateBatchById(list); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + Class clazz = bean.getClass(); + for (Method method : clazz.getDeclaredMethods()) { + Task task = method.getAnnotation(Task.class); + if (task != null) { + String value = task.value(); + String fname = StrUtil.isBlank(value) ? method.getName() : value; + if (fn.containsKey(fname)) { + log.warn("任务执行函数重复:{}", fname); + continue; + } else { + fn.put(fname, Tuple2.create(bean, method)); + log.info("任务执行函数:{}", fname); + } + + int period = task.period(); + String cron = task.cron(); + if (StrUtil.isNotBlank(cron) || period > 0) { + String memo_ = task.memo(); + String memo = StrUtil.isBlank(memo_) ? clazz.getCanonicalName() + "#" + method.getName() : memo_; + String scheduleConf = cron != null ? cron : String.valueOf(period); + ScheduleType scheduleType = StrUtil.isNotBlank(cron) ? ScheduleType.Cron : ScheduleType.Fixed; + tasks.add(new TaskInfo() + .setId(IdUtil.getSnowflakeNextId()) + .setMemo(memo) + .setFn(fname) + .setScheduleType(scheduleType) + .setScheduleConf(scheduleConf) + .setCriticalTiming(TaskUtil.computedNextTiming(scheduleType, scheduleConf)) + ); + log.info("添加任务:{}", memo); + } + } + } + return bean; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskUtil.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskUtil.java new file mode 100644 index 0000000..02c4fc3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskUtil.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.sichen.support; + +import com.njzscloud.common.sichen.contant.ScheduleType; +import lombok.extern.slf4j.Slf4j; + +import java.text.ParseException; +import java.util.Date; + +@Slf4j +public final class TaskUtil { + public static long computedNextTiming(long now, TaskInfo taskInfo) { + return computedNextTiming(now, taskInfo.getScheduleType(), taskInfo.getScheduleConf()); + } + + public static long computedNextTiming(TaskInfo taskInfo) { + return computedNextTiming(System.currentTimeMillis() / 1000, taskInfo.getScheduleType(), taskInfo.getScheduleConf()); + } + + public static long computedNextTiming(ScheduleType scheduleType, String scheduleConf) { + return computedNextTiming(System.currentTimeMillis() / 1000, scheduleType, scheduleConf); + } + + public static long computedNextTiming(long now, ScheduleType scheduleType, String scheduleConf) { + switch (scheduleType) { + case Fixed: + return new Date(now * 1000 + Long.parseLong(scheduleConf) * 1000L).getTime() / 1000; + case Cron: + try { + return new CronExpression(scheduleConf) + .getNextValidTimeAfter(new Date(now * 1000)) + .getTime() / 1000; + } catch (ParseException e) { + log.error("任务调度表达式解析错误", e); + return 0; + } + } + return 0; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..cf9406e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.sichen.config.TaskAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-sn/pom.xml b/njzscloud-common/njzscloud-common-sn/pom.xml new file mode 100644 index 0000000..4e3c9bd --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/pom.xml @@ -0,0 +1,42 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-sn + jar + + njzscloud-common-sn + http://maven.apache.org + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-mp + provided + + + com.njzscloud + njzscloud-common-mvc + + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java new file mode 100644 index 0000000..9ac04cf --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java @@ -0,0 +1,42 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import java.util.HashMap; +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@Constraint +public class AddSnConfigParam implements Constrained { + /** + * 编码名称; 字典编码:sncode + */ + @NotBlank(message = "编码名称不能为空") + private String sncode; + /** + * 配置 + */ + private List> config; + + /** + * 备注 + */ + private String memo; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> config != null && !config.isEmpty(), "编码规则不能为空"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java new file mode 100644 index 0000000..0df7e0d --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java @@ -0,0 +1,38 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.HashMap; +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@Constraint +public class ModifySnConfigParam implements Constrained { + private Long id; + + /** + * 配置 + */ + private List> config; + + /** + * 备注 + */ + private String memo; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> config != null && !config.isEmpty(), "编码规则不能为空"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnEntity.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnEntity.java new file mode 100644 index 0000000..de5c235 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnEntity.java @@ -0,0 +1,61 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.sn.contant.PadMode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; + +/** + * 递增编码表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_inc_sn") +public class SysIncSnEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * + */ + private String code; + + /** + * + */ + private Long val; + + /** + * + */ + private Integer step; + + /** + * + */ + private Long initialVal; + + /** + * + */ + private PadMode padMode; + + /** + * + */ + private String padVal; + + /** + * + */ + private Integer padLen; + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnMapper.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnMapper.java new file mode 100644 index 0000000..9b6800d --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 递增编码表 + */ +@Mapper +public interface SysIncSnMapper extends BaseMapper { + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnService.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnService.java new file mode 100644 index 0000000..62270e1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysIncSnService.java @@ -0,0 +1,77 @@ +package com.njzscloud.common.sn; + +import lombok.extern.slf4j.Slf4j; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 递增编码表 + */ +@Slf4j +public class SysIncSnService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysIncSnEntity 数据 + */ + public void add(SysIncSnEntity sysIncSnEntity) { + this.save(sysIncSnEntity); + } + + /** + * 修改 + * + * @param sysIncSnEntity 数据 + */ + public void modify(SysIncSnEntity sysIncSnEntity) { + this.updateById(sysIncSnEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysIncCodeEntity 结果 + */ + public SysIncSnEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysIncSnEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysIncCodeEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, SysIncSnEntity sysIncSnEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysIncSnEntity))); + } + + @Transactional(rollbackFor = Exception.class) + public Long inc(String code) { + SysIncSnEntity incCodeEntity = this.getOne(Wrappers.lambdaQuery() + .eq(SysIncSnEntity::getCode, code)); + Long val = incCodeEntity.getVal(); + val += incCodeEntity.getStep(); + this.updateById(incCodeEntity.setVal(val)); + return val; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigController.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigController.java new file mode 100644 index 0000000..4c07c74 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigController.java @@ -0,0 +1,86 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sn.support.SnUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 编码配置表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_sn_config") +@RequiredArgsConstructor +public class SysSnConfigController { + + private final SysSnConfigService sysSnConfigService; + + /** + * 新增 + * + * @param addSnConfigParam 数据 + */ + @PostMapping("/add") + public R add(@Validated @RequestBody AddSnConfigParam addSnConfigParam) { + sysSnConfigService.add(addSnConfigParam); + return R.success(); + } + + @GetMapping("/next") + public R next(@RequestParam(required = false, defaultValue = "Default") String sncode) { + return R.success(SnUtil.next(sncode)); + } + + /** + * 修改 + * + * @param sysSnConfigEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody ModifySnConfigParam modifySnConfigParam) { + sysSnConfigService.modify(modifySnConfigParam); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysSnConfigService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysSnConfigEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysSnConfigService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysSnConfigEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysSnConfigEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysSnConfigEntity sysSnConfigEntity) { + return R.success(sysSnConfigService.paging(pageParam, sysSnConfigEntity)); + } + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigEntity.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigEntity.java new file mode 100644 index 0000000..33a2918 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigEntity.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njzscloud.common.sn.support.SectionConfig; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 编码配置表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName(value = "sys_sn_config", autoResultMap = true) +public class SysSnConfigEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 编码名称 + */ + private String sncode; + + /** + * 配置 + */ + @TableField(typeHandler = com.njzscloud.common.mp.support.handler.j.JsonTypeHandler.class) + private List config; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigMapper.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigMapper.java new file mode 100644 index 0000000..7af3a14 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 编码配置表 + */ +@Mapper +public interface SysSnConfigMapper extends BaseMapper { + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigService.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigService.java new file mode 100644 index 0000000..a77207d --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SysSnConfigService.java @@ -0,0 +1,120 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sn.support.IncSectionConfig; +import com.njzscloud.common.sn.support.SectionConfig; +import com.njzscloud.common.sn.support.Sn; +import com.njzscloud.common.sn.support.SnUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 编码配置表 + */ +@Slf4j +@RequiredArgsConstructor +public class SysSnConfigService extends ServiceImpl implements IService { + private final SysIncSnService sysIncSnService; + + @Transactional(rollbackFor = Exception.class) + public Sn getSn(String sncode) { + SysSnConfigEntity configEntity = this.getOne(Wrappers.lambdaQuery().eq(SysSnConfigEntity::getSncode, sncode)); + if (configEntity == null) return new Sn(); + List configs = configEntity.getConfig(); + return new Sn(configs); + } + + /** + * 新增 + * + * @param addSnConfigParam 数据 + */ + @Transactional(rollbackFor = Exception.class) + public void add(AddSnConfigParam addSnConfigParam) { + List configs = addSnConfigParam.getConfig() + .stream() + .map(SnUtil::resolve) + .collect(Collectors.toList()); + + + this.save(new SysSnConfigEntity() + .setSncode(addSnConfigParam.getSncode()) + .setMemo(addSnConfigParam.getMemo()) + .setConfig(configs) + ); + + List collect = configs + .stream() + .filter(it -> it.getClass() == IncSectionConfig.class) + .map(it -> { + IncSectionConfig config = (IncSectionConfig) it; + return new SysIncSnEntity() + .setCode(config.getCode()) + .setVal((long) config.getInitialVal()) + .setStep(config.getStep()) + .setInitialVal((long) config.getInitialVal()) + .setPadMode(config.getPadMode()) + .setPadVal(config.getPadVal()) + .setPadLen(config.getPadLen()); + }).collect(Collectors.toList()); + if (collect.isEmpty()) return; + sysIncSnService.saveBatch(collect); + } + + /** + * 修改 + * + * @param modifySnConfigParam 数据 + */ + public void modify(ModifySnConfigParam modifySnConfigParam) { + List configs = modifySnConfigParam.getConfig() + .stream() + .map(SnUtil::resolve) + .collect(Collectors.toList()); + this.updateById(new SysSnConfigEntity() + .setId(modifySnConfigParam.getId()) + .setMemo(modifySnConfigParam.getMemo()) + .setConfig(configs) + ); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysSnConfigEntity 结果 + */ + public SysSnConfigEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysSnConfigEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysSnConfigEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, SysSnConfigEntity sysSnConfigEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysSnConfigEntity))); + } + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java new file mode 100644 index 0000000..10fca88 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.sn.config; + +import com.njzscloud.common.sn.SysIncSnService; +import com.njzscloud.common.sn.SysSnConfigController; +import com.njzscloud.common.sn.SysSnConfigService; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@MapperScan("com.njzscloud.common.sn") +@EnableConfigurationProperties(SnProperties.class) +public class SnAutoConfiguration { + + @Bean + public SysIncSnService sysIncSnService() { + return new SysIncSnService(); + } + + @Bean + public SysSnConfigService sysSnConfigService(SysIncSnService sysIncSnService) { + return new SysSnConfigService(sysIncSnService); + } + + @Bean + public SysSnConfigController sysSnConfigController(SysSnConfigService sysSnConfigService) { + return new SysSnConfigController(sysSnConfigService); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java new file mode 100644 index 0000000..e55b7b7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.sn.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@ConfigurationProperties(prefix = "sn") +public class SnProperties { +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java new file mode 100644 index 0000000..0bcbf69 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:pad_mode + * 字典名称:填充模式 + */ +@Getter +@RequiredArgsConstructor +public enum PadMode implements DictStr { + Wu("Wu", "无"), + Zuo("Zuo", "左"), + You("You", "右"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java new file mode 100644 index 0000000..4f43e8b --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:random_mode + * 字典名称:随机模式 + */ +@Getter +@RequiredArgsConstructor +public enum RandomMode implements DictStr { + SnowFlake("SnowFlake", "雪花码"), + UUID("UUID", "UUID"), + NanoId("NanoId", "NanoId"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java new file mode 100644 index 0000000..2187081 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:sn_section + * 字典名称:编码类型 + */ +@Getter +@RequiredArgsConstructor +public enum SnSection implements DictStr { + GuDing("GuDing", "固定"), + ShiJian("ShiJian", "时间"), + ZiZeng("ZiZeng", "自增"), + SuiJi("SuiJi", "随机"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java new file mode 100644 index 0000000..fada98c --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.sn.support; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class FixedSection implements ISnSection { + private final String value; + + @Override + public String next() { + return value; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java new file mode 100644 index 0000000..5131fc5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class FixedSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.GuDing; + private String value; + + @Override + public ISnSection section() { + return new FixedSection(value); + } + + @Override + public void resolve(Map config) { + value = MapUtil.getStr(config, "value"); + Assert.notBlank(value, () -> Exceptions.clierr("固定编码配置错误,未设置字段 value")); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java new file mode 100644 index 0000000..1b550c6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java @@ -0,0 +1,5 @@ +package com.njzscloud.common.sn.support; + +public interface ISnSection { + String next(); +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java new file mode 100644 index 0000000..fc4fcb4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.sn.SysIncSnService; +import com.njzscloud.common.sn.contant.PadMode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class IncSection implements ISnSection { + + private final String code; + private final Integer step; + private final Integer initialVal; + private final PadMode padMode; + private final String padVal; + private final Integer padLen; + private final SysIncSnService sysIncSnService = SpringUtil.getBean(SysIncSnService.class); + + @Override + public String next() { + Long val = sysIncSnService.inc(code); + return padMode == PadMode.Zuo ? StrUtil.padPre(val.toString(), padLen, padVal) : + padMode == PadMode.You ? StrUtil.padAfter(val.toString(), padLen, padVal) : + val.toString(); + } + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java new file mode 100644 index 0000000..86da9a7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java @@ -0,0 +1,50 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.sn.contant.PadMode; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class IncSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.ZiZeng; + private String code; + private Integer step; + private Integer initialVal; + private PadMode padMode; + private String padVal; + private Integer padLen; + + @Override + public ISnSection section() { + return new IncSection(code, step, initialVal, padMode, padVal, padLen); + } + + @Override + public void resolve(Map config) { + code = MapUtil.getStr(config, "code"); + step = MapUtil.getInt(config, "step"); + initialVal = MapUtil.getInt(config, "initialVal"); + padMode = Dict.parse(MapUtil.getStr(config, "padMode"), PadMode.values()); + padVal = MapUtil.getStr(config, "padVal"); + padLen = MapUtil.getInt(config, "padLen"); + Assert.notBlank(code, () -> Exceptions.clierr("递增编码配置错误,为设置字段 code")); + Assert.notNull(step, () -> Exceptions.clierr("递增编码配置错误,为设置字段 step")); + Assert.notNull(initialVal, () -> Exceptions.clierr("递增编码配置错误,为设置字段 initialVal")); + if (padMode != null) { + Assert.notBlank(padVal, () -> Exceptions.clierr("递增编码配置错误,为设置字段 padVal")); + Assert.notNull(padLen, () -> Exceptions.clierr("递增编码配置错误,为设置字段 padLen")); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java new file mode 100644 index 0000000..9285b97 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java @@ -0,0 +1,42 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.util.IdUtil; +import com.njzscloud.common.sn.contant.RandomMode; + +public class RandomSection implements ISnSection { + private final RandomMode randomMode; + private final Long workerId; + private final Long datacenterId; + private final Integer nanoIdSize; + + public RandomSection(RandomMode randomMode, Long workerId, Long datacenterId, Integer nanoIdSize) { + this.workerId = workerId; + this.datacenterId = datacenterId; + this.nanoIdSize = nanoIdSize; + this.randomMode = randomMode; + } + + public RandomSection(Long workerId, Long datacenterId) { + this(RandomMode.SnowFlake, workerId, datacenterId, 21); + } + + public RandomSection(Integer nanoIdSize) { + this(RandomMode.NanoId, null, null, nanoIdSize); + } + + public RandomSection() { + this(RandomMode.UUID, null, null, null); + } + + @Override + public String next() { + switch (randomMode) { + case UUID: + return IdUtil.simpleUUID(); + case NanoId: + return nanoIdSize == null ? IdUtil.nanoId() : IdUtil.nanoId(nanoIdSize); + default: + return workerId == null || datacenterId == null ? IdUtil.getSnowflakeNextIdStr() : IdUtil.getSnowflake(workerId, datacenterId).nextIdStr(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java new file mode 100644 index 0000000..8a873e3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java @@ -0,0 +1,53 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.sn.contant.RandomMode; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class RandomSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.SuiJi; + private RandomMode randomMode; + private Long workerId; + private Long datacenterId; + private Integer nanoIdSize; + + @Override + public ISnSection section() { + return new RandomSection(randomMode, workerId, datacenterId, nanoIdSize); + } + + @Override + public void resolve(Map config) { + randomMode = Dict.parse(MapUtil.getStr(config, "randomMode"), RandomMode.values()); + workerId = MapUtil.getLong(config, "workerId"); + datacenterId = MapUtil.getLong(config, "datacenterId"); + nanoIdSize = MapUtil.getInt(config, "nanoIdSize"); + Assert.notNull(randomMode, () -> Exceptions.clierr("随机编码配置错误,未设置字段 randomMode")); + if (randomMode == RandomMode.NanoId) { + Assert.notNull(nanoIdSize, () -> Exceptions.clierr("随机编码配置错误,未设置字段 nanoIdSize")); + Assert.isNull(workerId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 workerId")); + Assert.isNull(datacenterId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 datacenterId")); + } else if (randomMode == RandomMode.SnowFlake) { + Assert.notNull(workerId, () -> Exceptions.clierr("随机编码配置错误,未设置字段 workerId")); + Assert.notNull(datacenterId, () -> Exceptions.clierr("随机编码配置错误,未设置字段 datacenterId")); + Assert.isNull(nanoIdSize, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 nanoIdSize")); + } else { + Assert.isNull(workerId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 workerId")); + Assert.isNull(datacenterId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 datacenterId")); + Assert.isNull(nanoIdSize, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 nanoIdSize")); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java new file mode 100644 index 0000000..8724231 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.sn.support; + + +import com.njzscloud.common.sn.contant.SnSection; + +import java.util.Map; + +public interface SectionConfig { + + SnSection getSectionName(); + + ISnSection section(); + + void resolve(Map config); +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java new file mode 100644 index 0000000..9d22b68 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.sn.support; + +import java.util.List; +import java.util.stream.Collectors; + +public class Sn { + private final List sections; + + public Sn(List configs) { + this.sections = configs.stream().map(SectionConfig::section).collect(Collectors.toList()); + } + + public Sn() { + this.sections = null; + } + + public String next() { + if (sections == null || sections.isEmpty()) { + return null; + } + return sections.stream() + .map(ISnSection::next) + .reduce((a, b) -> a + b) + .orElseThrow(() -> new RuntimeException("No section found")); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java new file mode 100644 index 0000000..efa9f5e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.sn.SysSnConfigService; +import com.njzscloud.common.sn.contant.SnSection; + +import java.util.HashMap; +import java.util.Map; + +public final class SnUtil { + + private static final SysSnConfigService sysSnConfigService = SpringUtil.getBean(SysSnConfigService.class); + private static final Map> SECTIONS = new HashMap<>(); + + static { + SECTIONS.put(SnSection.GuDing, FixedSectionConfig.class); + SECTIONS.put(SnSection.ShiJian, TimeSectionConfig.class); + SECTIONS.put(SnSection.ZiZeng, IncSectionConfig.class); + SECTIONS.put(SnSection.SuiJi, RandomSectionConfig.class); + } + + public static SectionConfig resolve(Map config) { + String sectionName = MapUtil.getStr(config, "sectionName"); + Class sectionClass = SECTIONS.get(Dict.parse(sectionName, SnSection.values())); + SectionConfig sectionConfig; + try { + sectionConfig = sectionClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw Exceptions.error("配置解析失败"); + } + sectionConfig.resolve(config); + return sectionConfig; + } + + public static String next() { + return next("Default"); + } + + public static String next(String sncode) { + Assert.notBlank(sncode); + return sysSnConfigService.getSn(sncode).next(); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java new file mode 100644 index 0000000..0cff135 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.date.DateUtil; +import lombok.RequiredArgsConstructor; + +import java.util.Date; + +@RequiredArgsConstructor +public class TimeSection implements ISnSection { + + private final String pattern; + private final Boolean timestamp; + private final Integer unit; + + @Override + public String next() { + return timestamp ? new Date().getTime() / unit + "" : DateUtil.format(new Date(), pattern); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java new file mode 100644 index 0000000..69b596c --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class TimeSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.ShiJian; + private String pattern; + private Boolean timestamp; + private Integer unit; + + @Override + public ISnSection section() { + return new TimeSection(pattern, timestamp, unit); + } + + @Override + public void resolve(Map config) { + pattern = MapUtil.getStr(config, "pattern"); + timestamp = MapUtil.getBool(config, "timestamp", false); + unit = MapUtil.getInt(config, "unit"); + if (timestamp) { + Assert.notNull(unit, () -> Exceptions.clierr("时间编码配置错误,为设置字段 unit")); + Assert.isTrue(StrUtil.isBlank(pattern), () -> Exceptions.clierr("时间编码配置错误,为设置字段 pattern 与 timestamp 不能同时使用")); + + } else { + Assert.notBlank(pattern, () -> Exceptions.clierr("时间编码配置错误,为设置字段 pattern")); + Assert.isTrue(unit == null, () -> Exceptions.clierr("时间编码配置错误,为设置字段 unit 与 pattern 不能同时使用")); + + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..296d0fb --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ + com.njzscloud.common.sn.config.SnAutoConfiguration diff --git a/njzscloud-common/pom.xml b/njzscloud-common/pom.xml new file mode 100644 index 0000000..3fcc09d --- /dev/null +++ b/njzscloud-common/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.njzscloud + zsy-recycling-supervision + 0.0.1 + + + njzscloud-common + + pom + + + njzscloud-common-core + njzscloud-common-security + njzscloud-common-mp + njzscloud-common-mvc + njzscloud-common-redis + njzscloud-common-minio + njzscloud-common-email + njzscloud-common-job + njzscloud-common-cache + njzscloud-common-sichen + njzscloud-common-sn + njzscloud-common-gen + + + + diff --git a/njzscloud-svr/pom.xml b/njzscloud-svr/pom.xml new file mode 100644 index 0000000..ef9a991 --- /dev/null +++ b/njzscloud-svr/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + com.njzscloud + zsy-recycling-supervision + 0.0.1 + + njzscloud-svr + jar + + 8 + 8 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + + + com.njzscloud + njzscloud-common-mp + + + com.njzscloud + njzscloud-common-mvc + + + com.njzscloud + njzscloud-common-sichen + + + com.njzscloud + njzscloud-common-sn + + + + com.njzscloud + njzscloud-common-security + + + com.njzscloud + njzscloud-common-gen + + + + jakarta.validation + jakarta.validation-api + + + + org.hibernate.validator + hibernate-validator + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + org.projectlombok + lombok + 1.18.30 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java new file mode 100644 index 0000000..f325c6b --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java @@ -0,0 +1,12 @@ +package com.njzscloud.supervisory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/mapper/SysTokenMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/mapper/SysTokenMapper.java new file mode 100644 index 0000000..c924794 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/mapper/SysTokenMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.auth.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.auth.pojo.SysTokenEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 登录令牌表 + */ +@Mapper +public interface SysTokenMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/pojo/SysTokenEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/pojo/SysTokenEntity.java new file mode 100644 index 0000000..9f531dd --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/pojo/SysTokenEntity.java @@ -0,0 +1,50 @@ +package com.njzscloud.supervisory.auth.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; + +/** + * 登录令牌表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_token") +public class SysTokenEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 Id + */ + private Long userId; + + /** + * Token Id + */ + private String tid; + + /** + * Token 键 + */ + private String tkey; + + /** + * Token 值 + */ + private String tval; + + /** + * 用户信息 + */ + private String userDetail; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/SecurityService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/SecurityService.java new file mode 100644 index 0000000..3f185aa --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/SecurityService.java @@ -0,0 +1,36 @@ +package com.njzscloud.supervisory.auth.service; + +import com.njzscloud.common.security.support.*; +import com.njzscloud.supervisory.resource.service.SysResourceService; +import com.njzscloud.supervisory.role.service.SysRoleService; +import com.njzscloud.supervisory.user.service.SysUserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SecurityService implements IUserService, IResourceService, IRoleService { + + private final SysUserService sysUserService; + private final SysRoleService sysRoleService; + private final SysResourceService sysResourceService; + + @Override + public UserDetail selectUserByAccount(String account) { + return sysUserService.getUserInfo(account); + } + + @Override + public Resource selectResourceByUserId(Long userId) { + return sysResourceService.selectResourceByUserId(userId); + } + + @Override + public Set selectRoleByUserId(Long userId) { + return sysRoleService.listUserRole(userId); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/TokenService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/TokenService.java new file mode 100644 index 0000000..ede6145 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/auth/service/TokenService.java @@ -0,0 +1,68 @@ +package com.njzscloud.supervisory.auth.service; + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.fastjson.Fastjson; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.support.ITokenService; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.supervisory.auth.mapper.SysTokenMapper; +import com.njzscloud.supervisory.auth.pojo.SysTokenEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TokenService implements ITokenService { + private static final TimedCache TOKEN_CACHE = CacheUtil.newTimedCache(1000 * 3600); + private final SysTokenMapper sysTokenMapper; + + @Override + public synchronized void saveToken(UserDetail userDetail) { + Token token = userDetail.getToken(); + long userId = token.getUserId(); + String tid = token.getTid(); + String key = Constants.TOKEN_CACHE_KEY.fill(userId, tid); + Long l = sysTokenMapper.selectCount(Wrappers.lambdaQuery().eq(SysTokenEntity::getUserId, token.getUserId())); + if (l > 0) return; + sysTokenMapper.insert(new SysTokenEntity() + .setUserId(userId) + .setTid(tid) + .setTkey(key) + .setTval(token.toString()) + .setUserDetail(userDetail.toString()) + ); + } + + @Override + public UserDetail loadUser(String tokenStr) { + Token token = Token.create(tokenStr); + long userId = token.getUserId(); + return TOKEN_CACHE.get(userId, () -> Fastjson.toBean(sysTokenMapper + .selectOne(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId)) + .getUserDetail(), + UserDetail.class)); + } + + @Override + public void removeToken(Token token) { + long userId = token.getUserId(); + TOKEN_CACHE.remove(userId); + sysTokenMapper.delete(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId) + ); + TOKEN_CACHE.remove(userId); + } + + @Override + public void removeToken(Long userId) { + TOKEN_CACHE.remove(userId); + sysTokenMapper.delete(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId) + ); + TOKEN_CACHE.remove(userId); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictController.java new file mode 100644 index 0000000..a2a4ca7 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictController.java @@ -0,0 +1,94 @@ +package com.njzscloud.supervisory.dict.controller; + +import com.njzscloud.supervisory.dict.pojo.SysDictEntity; +import com.njzscloud.supervisory.dict.pojo.ObtainDictDataResult; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.dict.service.SysDictService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 字典表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_dict") +@RequiredArgsConstructor +public class SysDictController { + + private final SysDictService sysDictService; + + /** + * 新增 + * + * @param sysDictEntity 数据 + */ + @PostMapping("/add") + public R add(@RequestBody SysDictEntity sysDictEntity) { + sysDictService.add(sysDictEntity); + return R.success(); + } + + /** + * 修改 + * + * @param sysDictEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody SysDictEntity sysDictEntity) { + sysDictService.modify(sysDictEntity); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysDictService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysDictEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysDictService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysDictEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysDictEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysDictEntity sysDictEntity) { + return R.success(sysDictService.paging(pageParam, sysDictEntity)); + } + + + /** + * 获取字典数据 + * + * @param dictKey 字典标识 + */ + @GetMapping("/dict_data") + public R> obtainDictData(@RequestParam String dictKey) { + return R.success(sysDictService.obtainDictData(dictKey)); + } + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictItemController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictItemController.java new file mode 100644 index 0000000..6ad4d0f --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/controller/SysDictItemController.java @@ -0,0 +1,81 @@ +package com.njzscloud.supervisory.dict.controller; + +import com.njzscloud.supervisory.dict.pojo.SysDictItemEntity; +import com.njzscloud.supervisory.dict.service.SysDictItemService; +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; + +import java.util.List; + +/** + * 字典条目表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_dict_item") +@RequiredArgsConstructor +public class SysDictItemController { + + private final SysDictItemService sysDictItemService; + + /** + * 新增 + * + * @param sysDictItemEntity 数据 + */ + @PostMapping("/add") + public R add(@RequestBody SysDictItemEntity sysDictItemEntity) { + sysDictItemService.add(sysDictItemEntity); + return R.success(); + } + + /** + * 修改 + * + * @param sysDictItemEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody SysDictItemEntity sysDictItemEntity) { + sysDictItemService.modify(sysDictItemEntity); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysDictItemService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysDictItemEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysDictItemService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysDictItemEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysDictItemEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysDictItemEntity sysDictItemEntity) { + return R.success(sysDictItemService.paging(pageParam, sysDictItemEntity)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictItemMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictItemMapper.java new file mode 100644 index 0000000..b7e0af5 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictItemMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.dict.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.dict.pojo.SysDictItemEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典条目表 + */ +@Mapper +public interface SysDictItemMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictMapper.java new file mode 100644 index 0000000..313c3e1 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/mapper/SysDictMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.dict.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.dict.pojo.SysDictEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典表 + */ +@Mapper +public interface SysDictMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/ObtainDictDataResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/ObtainDictDataResult.java new file mode 100644 index 0000000..f257d8b --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/ObtainDictDataResult.java @@ -0,0 +1,60 @@ +package com.njzscloud.supervisory.dict.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 字典条目表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class ObtainDictDataResult { + + /** + * Id + */ + private Long id; + + /** + * 字典 Id; sys_dict.id + */ + private Long dictId; + + /** + * 字典标识; sys_dict.dict_key + */ + private String dictKey; + + /** + * 值; 分类值/字典项值 + */ + private String val; + + /** + * 显示文本; 分类显示文本/字典项显示文本 + */ + private String txt; + + /** + * 排序 + */ + private Integer sort; + + /** + * 备注 + */ + private String memo; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictEntity.java new file mode 100644 index 0000000..e2f8fba --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictEntity.java @@ -0,0 +1,44 @@ +package com.njzscloud.supervisory.dict.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; + +/** + * 字典表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_dict") +public class SysDictEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 字典标识 + */ + private String dictKey; + + /** + * 字典名称 + */ + private String dictName; + + /** + * 备注 + */ + private String memo; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictItemEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictItemEntity.java new file mode 100644 index 0000000..10f88ea --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/pojo/SysDictItemEntity.java @@ -0,0 +1,59 @@ +package com.njzscloud.supervisory.dict.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; + +/** + * 字典条目表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_dict_item") +public class SysDictItemEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 字典 Id; sys_dict.id + */ + private Long dictId; + + /** + * 字典标识; sys_dict.dict_key + */ + private String dictKey; + + /** + * 值; 分类值/字典项值 + */ + private String val; + + /** + * 显示文本; 分类显示文本/字典项显示文本 + */ + private String txt; + + /** + * 排序 + */ + private Integer sort; + + /** + * 备注 + */ + private String memo; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictItemService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictItemService.java new file mode 100644 index 0000000..993f17c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictItemService.java @@ -0,0 +1,77 @@ +package com.njzscloud.supervisory.dict.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.dict.mapper.SysDictItemMapper; +import com.njzscloud.supervisory.dict.pojo.SysDictItemEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 字典条目表 + */ +@Slf4j +@Service +public class SysDictItemService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysDictItemEntity 数据 + */ + + public void add(SysDictItemEntity sysDictItemEntity) { + this.save(sysDictItemEntity); + } + + /** + * 修改 + * + * @param sysDictItemEntity 数据 + */ + + public void modify(SysDictItemEntity sysDictItemEntity) { + this.updateById(sysDictItemEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysDictItemEntity 结果 + */ + + public SysDictItemEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysDictItemEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysDictItemEntity> 分页结果 + */ + + public PageResult paging(PageParam pageParam, SysDictItemEntity sysDictItemEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysDictItemEntity))); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictService.java new file mode 100644 index 0000000..76605d6 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/dict/service/SysDictService.java @@ -0,0 +1,91 @@ +package com.njzscloud.supervisory.dict.service; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.dict.mapper.SysDictMapper; +import com.njzscloud.supervisory.dict.pojo.SysDictEntity; +import com.njzscloud.supervisory.dict.pojo.SysDictItemEntity; +import com.njzscloud.supervisory.dict.pojo.ObtainDictDataResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 字典表 + */ +@Slf4j +@Service +public class SysDictService extends ServiceImpl implements IService { + @Autowired + private SysDictItemService sysDictItemService; + + /** + * 新增 + * + * @param sysDictEntity 数据 + */ + + public void add(SysDictEntity sysDictEntity) { + this.save(sysDictEntity); + } + + /** + * 修改 + * + * @param sysDictEntity 数据 + */ + + public void modify(SysDictEntity sysDictEntity) { + this.updateById(sysDictEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysDictEntity 结果 + */ + + public SysDictEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysDictEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysDictEntity> 分页结果 + */ + + public PageResult paging(PageParam pageParam, SysDictEntity sysDictEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysDictEntity))); + } + + + public List obtainDictData(String dictKey) { + return sysDictItemService.list(Wrappers.lambdaQuery().eq(SysDictItemEntity::getDictKey, dictKey)) + .stream().map(it -> BeanUtil.copyProperties(it, ObtainDictDataResult.class)) + .collect(Collectors.toList()); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/controller/SysDistrictController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/controller/SysDistrictController.java new file mode 100644 index 0000000..45f5153 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/controller/SysDistrictController.java @@ -0,0 +1,87 @@ +package com.njzscloud.supervisory.district.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.district.pojo.DistrictTreeResult; +import com.njzscloud.supervisory.district.pojo.SysDistrictEntity; +import com.njzscloud.supervisory.district.service.SysDistrictService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 省市区表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_district") +@RequiredArgsConstructor +public class SysDistrictController { + + private final SysDistrictService sysDistrictService; + + /** + * 新增 + * + * @param sysDistrictEntity 数据 + */ + @PostMapping("/add") + public R add(@RequestBody SysDistrictEntity sysDistrictEntity) { + sysDistrictService.add(sysDistrictEntity); + return R.success(); + } + + /** + * 修改 + * + * @param sysDistrictEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody SysDistrictEntity sysDistrictEntity) { + sysDistrictService.modify(sysDistrictEntity); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysDistrictService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysDistrictEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysDistrictService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysDistrictEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysDistrictEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysDistrictEntity sysDistrictEntity) { + return R.success(sysDistrictService.paging(pageParam, sysDistrictEntity)); + } + + @GetMapping("/tree") + public R> tree() { + return R.success(sysDistrictService.tree()); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/mapper/SysDistrictMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/mapper/SysDistrictMapper.java new file mode 100644 index 0000000..37c7c7c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/mapper/SysDistrictMapper.java @@ -0,0 +1,32 @@ +package com.njzscloud.supervisory.district.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.district.pojo.DistrictTreeResult; +import com.njzscloud.supervisory.district.pojo.SysDistrictEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 省市区表 + */ +@Mapper +public interface SysDistrictMapper extends BaseMapper { + + @Select("SELECT id,\n" + + " province,\n" + + " city,\n" + + " area,\n" + + " town,\n" + + " IF(tier = 3, 0, pid) pid,\n" + + " tier,\n" + + " district_name,\n" + + " sort\n" + + "FROM sys_district\n" + + "WHERE tier >= 3 \n" + + "AND province = '${province}' \n" + + "AND city = '${city}'") + List selectByTier(@Param("province") String province, @Param("city") String city); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/DistrictTreeResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/DistrictTreeResult.java new file mode 100644 index 0000000..1a68238 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/DistrictTreeResult.java @@ -0,0 +1,64 @@ +package com.njzscloud.supervisory.district.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +public class DistrictTreeResult { + + /** + * Id; 地区代码 + */ + private String id; + + /** + * 上级地区代码 + */ + private String pid; + + /** + * 省 + */ + private String province; + + /** + * 市 + */ + private String city; + + /** + * 区县 + */ + private String area; + + /** + * 乡镇街道 + */ + private String town; + + /** + * 层级; >= 1 + */ + + private Integer tier; + + /** + * 地区名称 + */ + + private String districtName; + + /** + * 排序 + */ + + private Integer sort; + + private List children; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/SysDistrictEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/SysDistrictEntity.java new file mode 100644 index 0000000..a19b6ef --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/pojo/SysDistrictEntity.java @@ -0,0 +1,65 @@ +package com.njzscloud.supervisory.district.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; + +/** + * 省市区表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_district") +public class SysDistrictEntity { + + /** + * Id; 地区代码 + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 上级地区代码 + */ + private String pid; + + /** + * 省 + */ + private String province; + + /** + * 市 + */ + private String city; + + /** + * 区县 + */ + private String area; + + /** + * 乡镇街道 + */ + private String town; + + /** + * 层级; >= 1 + */ + private Integer tier; + + /** + * 地区名称 + */ + private String districtName; + + /** + * 排序 + */ + private Integer sort; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/service/SysDistrictService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/service/SysDistrictService.java new file mode 100644 index 0000000..d033931 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/district/service/SysDistrictService.java @@ -0,0 +1,84 @@ +package com.njzscloud.supervisory.district.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.tree.Tree; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.district.mapper.SysDistrictMapper; +import com.njzscloud.supervisory.district.pojo.DistrictTreeResult; +import com.njzscloud.supervisory.district.pojo.SysDistrictEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 省市区表 + */ +@Slf4j +@Service +public class SysDistrictService extends ServiceImpl implements IService { + + @Value("${app.district.province}") + private String province; + @Value("${app.district.city}") + private String city; + + /** + * 新增 + * + * @param sysDistrictEntity 数据 + */ + public void add(SysDistrictEntity sysDistrictEntity) { + this.save(sysDistrictEntity); + } + + /** + * 修改 + * + * @param sysDistrictEntity 数据 + */ + public void modify(SysDistrictEntity sysDistrictEntity) { + this.updateById(sysDistrictEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysDistrictEntity 结果 + */ + public SysDistrictEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysDistrictEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysDistrictEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, SysDistrictEntity sysDistrictEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysDistrictEntity))); + } + + public List tree() { + List list = baseMapper.selectByTier(province, city); + return Tree.listToTree(list, DistrictTreeResult::getId, DistrictTreeResult::getPid, DistrictTreeResult::setChildren, "0"); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/contant/RequestMethod.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/contant/RequestMethod.java new file mode 100644 index 0000000..36a2f8b --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/contant/RequestMethod.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.endpoint.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:request_method + * 字典名称:HTTP 请求方式 + */ +@Getter +@RequiredArgsConstructor +public enum RequestMethod implements DictStr { + GET("GET", "GET 请求"), + POST("POST", "POST 请求"), + PUT("PUT", "PUT 请求"), + DELETE("DELETE", "DELETE 请求"); + + private final String val; + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/controller/SysEndpointController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/controller/SysEndpointController.java new file mode 100644 index 0000000..51c0bed --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/controller/SysEndpointController.java @@ -0,0 +1,81 @@ +package com.njzscloud.supervisory.endpoint.controller; + +import com.njzscloud.supervisory.endpoint.pojo.SysEndpointEntity; +import com.njzscloud.supervisory.endpoint.service.SysEndpointService; +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; + +import java.util.List; + +/** + * 端点信息表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_endpoint") +@RequiredArgsConstructor +public class SysEndpointController { + + private final SysEndpointService sysEndpointService; + + /** + * 新增 + * + * @param sysEndpointEntity 数据 + */ + @PostMapping("/add") + public R add(@RequestBody SysEndpointEntity sysEndpointEntity) { + sysEndpointService.add(sysEndpointEntity); + return R.success(); + } + + /** + * 修改 + * + * @param sysEndpointEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody SysEndpointEntity sysEndpointEntity) { + sysEndpointService.modify(sysEndpointEntity); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysEndpointService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysEndpointEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysEndpointService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysEndpointEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysEndpointEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysEndpointEntity sysEndpointEntity) { + return R.success(sysEndpointService.paging(pageParam, sysEndpointEntity)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/mapper/SysEndpointMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/mapper/SysEndpointMapper.java new file mode 100644 index 0000000..ecd186e --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/mapper/SysEndpointMapper.java @@ -0,0 +1,16 @@ +package com.njzscloud.supervisory.endpoint.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.endpoint.pojo.SysEndpointEntity; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 端点信息表 + */ +@Mapper +public interface SysEndpointMapper extends BaseMapper { + + List listUserEndpoint(Long userId); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/pojo/SysEndpointEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/pojo/SysEndpointEntity.java new file mode 100644 index 0000000..ff8f95c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/pojo/SysEndpointEntity.java @@ -0,0 +1,85 @@ +package com.njzscloud.supervisory.endpoint.pojo; + +import com.njzscloud.common.security.contant.EndpointAccessModel; +import com.njzscloud.supervisory.endpoint.contant.RequestMethod; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.time.LocalDateTime; + +/** + * 端点信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_endpoint") +public class SysEndpointEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 请求方式; 字典代码:request_method + */ + private RequestMethod requestMethod; + + /** + * 路由前缀; 以 / 开头 或 为空 + */ + private String routingPath; + + /** + * 端点地址; 以 / 开头, Ant 匹配模式 + */ + private String endpointPath; + + /** + * 接口访问模式; 字典代码:endpoint_access_model + */ + private EndpointAccessModel accessModel; + + /** + * 备注 + */ + private String memo; + + /** + * 创建人 Id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/service/SysEndpointService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/service/SysEndpointService.java new file mode 100644 index 0000000..92141df --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/endpoint/service/SysEndpointService.java @@ -0,0 +1,85 @@ +package com.njzscloud.supervisory.endpoint.service; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.security.support.EndpointResource; +import com.njzscloud.supervisory.endpoint.mapper.SysEndpointMapper; +import com.njzscloud.supervisory.endpoint.pojo.SysEndpointEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 端点信息表 + */ +@Slf4j +@Service +public class SysEndpointService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysEndpointEntity 数据 + */ + public void add(SysEndpointEntity sysEndpointEntity) { + this.save(sysEndpointEntity); + } + + /** + * 修改 + * + * @param sysEndpointEntity 数据 + */ + public void modify(SysEndpointEntity sysEndpointEntity) { + this.updateById(sysEndpointEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysEndpointEntity 结果 + */ + public SysEndpointEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysEndpointEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysEndpointEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, SysEndpointEntity sysEndpointEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysEndpointEntity))); + } + + public List listUserEndpoint(Long userId) { + List sysMenuEntities = baseMapper.listUserEndpoint(userId); + return sysMenuEntities.stream() + .map(it -> BeanUtil.copyProperties(it, EndpointResource.class) + .setAccessModel(it.getAccessModel().getVal()) + .setRequestMethod(it.getRequestMethod().getVal()) + + ) + .collect(Collectors.toList()); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/contant/MenuCategory.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/contant/MenuCategory.java new file mode 100644 index 0000000..792b769 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/contant/MenuCategory.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.menu.contant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import com.njzscloud.common.core.ienum.DictStr; + +/** + * 菜单类型 + */ +@Getter +@RequiredArgsConstructor +public enum MenuCategory implements DictStr { + Catalog("Catalog", "目录"), + Group("Group", "组"), + Page("Page", "页面"), + SubPage("SubPage", "子页面"), + Btn("Btn", "按钮"); + + private final String val; + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/controller/SysMenuController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/controller/SysMenuController.java new file mode 100644 index 0000000..52d1def --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/controller/SysMenuController.java @@ -0,0 +1,97 @@ +package com.njzscloud.supervisory.menu.controller; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.security.support.MenuResource; +import com.njzscloud.supervisory.menu.pojo.MenuAddParam; +import com.njzscloud.supervisory.menu.pojo.MenuModifyParam; +import com.njzscloud.supervisory.menu.pojo.MenuSearchParam; +import com.njzscloud.supervisory.menu.pojo.MenuDetailResult; +import com.njzscloud.supervisory.menu.service.SysMenuService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 菜单 + */ +@Slf4j +@RestController +@RequestMapping("/sys_menu") +@RequiredArgsConstructor +public class SysMenuController { + + private final SysMenuService sysMenuService; + + /** + * 新增
+ * + * @param menuAddParam 数据 + * @required + */ + @PostMapping("/add") + public R add(@RequestBody @Validated MenuAddParam menuAddParam) { + return R.success(sysMenuService.add(menuAddParam)); + } + + /** + * 修改 + * + * @param menuModifyParam 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody @Validated MenuModifyParam menuModifyParam) { + sysMenuService.modify(menuModifyParam); + return R.success(); + } + + /** + * 删除 + * + * @param ids Id + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, "未指定要删除的数据"); + sysMenuService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysMenuVO 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysMenuService.detail(id)); + } + + /** + * 查询 + */ + @GetMapping("/list") + public R> list(@RequestParam(required = false) Long pid) { + return R.success(sysMenuService.list(pid)); + } + + /** + * 查询 + */ + @GetMapping("/page_list") + public R> pageList(PageParam pageParam, MenuSearchParam menuSearchParam) { + return R.success(sysMenuService.pageList(pageParam.toPage(), menuSearchParam)); + } + + @GetMapping("/user_menu") + public R> listUserMenu(@RequestParam Long userId) { + return R.success(sysMenuService.listUserMenu(userId)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/mapper/SysMenuMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/mapper/SysMenuMapper.java new file mode 100644 index 0000000..c5d9946 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/mapper/SysMenuMapper.java @@ -0,0 +1,15 @@ +package com.njzscloud.supervisory.menu.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.menu.pojo.SysMenuEntity; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 菜单信息表 + */ +@Mapper +public interface SysMenuMapper extends BaseMapper { + List selectMenuByUserId(Long userId); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuAddParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuAddParam.java new file mode 100644 index 0000000..f6c906f --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuAddParam.java @@ -0,0 +1,74 @@ +package com.njzscloud.supervisory.menu.pojo; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.supervisory.menu.contant.MenuCategory; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; + +/** + * 添加菜单 + */ +@Getter +@Setter +@Constraint +public class MenuAddParam implements Constrained { + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + @NotBlank(message = "菜单名称不能为空") + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 类型; 字典代码:menu_category + */ + private MenuCategory menuCategory; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + private String sn; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> pid == null || pid >= 0, "上级 Id 必须大于 0 或不指定"), + ValidRule.of(() -> menuCategory != null, "类型不能为空"), + ValidRule.of(() -> { + if ((menuCategory == MenuCategory.Catalog || menuCategory == MenuCategory.Btn)) { + return StrUtil.isBlank(routeName); + } else { + return true; + } + }, "菜单目录和按钮不用指定路由名称"), + ValidRule.of(() -> { + if (menuCategory == MenuCategory.Page || menuCategory == MenuCategory.SubPage) { + return StrUtil.isNotBlank(routeName); + } else { + return true; + } + }, "路由名称不能为空"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuDetailResult.java new file mode 100644 index 0000000..0e3c846 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuDetailResult.java @@ -0,0 +1,57 @@ +package com.njzscloud.supervisory.menu.pojo; + +import com.njzscloud.supervisory.menu.contant.MenuCategory; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 添加菜单 + */ +@Getter +@Setter +@EqualsAndHashCode +@Accessors(chain = true) +public class MenuDetailResult { + + /** + * 主键 Id + */ + private Long id; + + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + private Integer tier; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 类型; 字典代码:menu_category + */ + private MenuCategory menuCategory; + + private List breadcrumb; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuModifyParam.java new file mode 100644 index 0000000..76d9447 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuModifyParam.java @@ -0,0 +1,56 @@ +package com.njzscloud.supervisory.menu.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; + +/** + * 添加菜单 + */ +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class MenuModifyParam implements Constrained { + + /** + * Id + */ + private Long id; + + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> id != null, "未知定要修改的菜单"), + ValidRule.of(() -> pid == null || pid >= 0, "上级 Id 必须大于 0 或不指定"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuSearchParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuSearchParam.java new file mode 100644 index 0000000..df42817 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/MenuSearchParam.java @@ -0,0 +1,27 @@ +package com.njzscloud.supervisory.menu.pojo; + +import com.njzscloud.common.mvc.validator.Constraint; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 添加菜单 + */ +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class MenuSearchParam { + + /** + * 菜单名称 + */ + private String title; + + + /** + * 路由名称 + */ + private String routeName; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/SysMenuEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/SysMenuEntity.java new file mode 100644 index 0000000..963b6d2 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/pojo/SysMenuEntity.java @@ -0,0 +1,100 @@ +package com.njzscloud.supervisory.menu.pojo; + +import com.baomidou.mybatisplus.annotation.*; +import com.njzscloud.common.mp.support.handler.j.JsonTypeHandler; +import com.njzscloud.supervisory.menu.contant.MenuCategory; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 菜单信息表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName(value = "sys_menu", autoResultMap = true) +public class SysMenuEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private String sn; + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 层级; >= 1 + */ + private Integer tier; + + /** + * 面包路径; 逗号分隔 + */ + @TableField(value = "breadcrumb", typeHandler = JsonTypeHandler.class) + private List breadcrumb; + + /** + * 类型; 字典代码:menu_category + */ + private MenuCategory menuCategory; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/service/SysMenuService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/service/SysMenuService.java new file mode 100644 index 0000000..f04c99d --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/menu/service/SysMenuService.java @@ -0,0 +1,196 @@ +package com.njzscloud.supervisory.menu.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.security.support.MenuResource; +import com.njzscloud.supervisory.menu.contant.MenuCategory; +import com.njzscloud.supervisory.menu.mapper.SysMenuMapper; +import com.njzscloud.supervisory.menu.pojo.*; +import com.njzscloud.supervisory.resource.pojo.SysResourceEntity; +import com.njzscloud.supervisory.resource.service.SysResourceService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

sys_menu

+ *

菜单信息表

+ */ +@Slf4j +@Service +public class SysMenuService extends ServiceImpl implements IService { + + @Autowired + private SysResourceService sysResourceService; + + /** + * 新增 + * + * @param menuAddParam 数据 + * @return + */ + + @Transactional(rollbackFor = Exception.class) + public Long add(MenuAddParam menuAddParam) { + String sn = menuAddParam.getSn(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(SysMenuEntity::getSn, sn)); + Assert.isFalse(exists, () -> Exceptions.exception("资源编码重复")); + SysMenuEntity sysMenuEntity = BeanUtil.copyProperties(menuAddParam, SysMenuEntity.class); + Long pid = sysMenuEntity.getPid(); + if (pid == null) { + sysMenuEntity.setPid(0L); + pid = 0L; + } + String title = sysMenuEntity.getTitle(); + if (pid == 0) { + sysMenuEntity.setTier(1); + sysMenuEntity.setBreadcrumb(CollUtil.newArrayList(title)); + } else { + SysMenuEntity parent = this.getById(pid); + Assert.notNull(parent, () -> Exceptions.exception("上级菜单不存在")); + sysMenuEntity.setTier(parent.getTier() + 1); + List breadcrumb = parent.getBreadcrumb(); + breadcrumb.add(title); + sysMenuEntity.setBreadcrumb(breadcrumb); + } + this.save(sysMenuEntity); + Long sysMenuEntityId = sysMenuEntity.getId(); + exists = sysResourceService.exists(Wrappers.lambdaQuery().eq(SysResourceEntity::getSn, sn)); + Assert.isFalse(exists, () -> Exceptions.exception("资源编码重复")); + sysResourceService.save(new SysResourceEntity() + .setSn(sn) + .setDataId(sysMenuEntityId) + .setTableName("sys_menu") + .setMemo("菜单资源-" + menuAddParam.getMenuCategory().getTxt() + "-" + menuAddParam.getTitle()) + ); + return sysMenuEntityId; + } + + /** + * 修改 + * + * @param menuModifyParam 数据 + */ + + @Transactional(rollbackFor = Exception.class) + public void modify(MenuModifyParam menuModifyParam) { + SysMenuEntity sysMenuEntity = BeanUtil.copyProperties(menuModifyParam, SysMenuEntity.class); + Long id = sysMenuEntity.getId(); + Long pid = sysMenuEntity.getPid(); + SysMenuEntity oldSysMenuEntity = this.getById(id); + Assert.notNull(oldSysMenuEntity, () -> Exceptions.exception("菜单不存在")); + MenuCategory menuCategory = oldSysMenuEntity.getMenuCategory(); + String routeName = sysMenuEntity.getRouteName(); + + if (menuCategory == MenuCategory.Page || menuCategory == MenuCategory.SubPage && routeName != null) { + Assert.notBlank(routeName, () -> Exceptions.exception("路由名称不能为空")); + } else if (menuCategory == MenuCategory.Catalog || menuCategory == MenuCategory.Btn && routeName != null) { + Assert.isTrue(StrUtil.isBlank(routeName), () -> Exceptions.exception("菜单目录和按钮不用指定路由名称")); + } + + if (pid != null && !pid.equals(oldSysMenuEntity.getPid()) && menuCategory == MenuCategory.Catalog) { + SysMenuEntity parent = this.getById(pid); + String title = sysMenuEntity.getTitle(); + List breadcrumb = parent.getBreadcrumb(); + breadcrumb.add(StrUtil.isBlank(title) ? oldSysMenuEntity.getTitle() : title); + sysMenuEntity.setBreadcrumb(breadcrumb) + .setTier(parent.getTier() + 1); + + this.modifyChild(sysMenuEntity); + } + + this.updateById(sysMenuEntity); + + String title = menuModifyParam.getTitle(); + if (StrUtil.isBlank(title)) return; + sysResourceService.update(Wrappers.lambdaUpdate() + .eq(SysResourceEntity::getTableName, "sys_menu") + .eq(SysResourceEntity::getDataId, id) + .set(SysResourceEntity::getMemo, "菜单资源-" + oldSysMenuEntity.getMenuCategory().getTxt() + "-" + title)); + } + + private void modifyChild(SysMenuEntity sysMenuEntity) { + List breadcrumb = sysMenuEntity.getBreadcrumb(); + int tier = sysMenuEntity.getTier() + 1; + this.list(Wrappers.lambdaQuery().eq(SysMenuEntity::getPid, sysMenuEntity.getId())) + .stream() + .map(it -> { + List temp = CollUtil.newArrayList(breadcrumb); + temp.add(it.getTitle()); + return new SysMenuEntity() + .setId(it.getId()) + .setBreadcrumb(temp) + .setTier(tier); + }) + .forEach(this::modifyChild); + } + + /** + * 删除 + * + * @param ids Id + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + sysResourceService.remove(Wrappers.lambdaQuery() + .eq(SysResourceEntity::getTableName, "sys_menu") + .in(SysResourceEntity::getDataId, ids) + ); + } + + /** + * 详情 + * + * @param id Id + * @return SysMenuListVO 结果 + */ + + public MenuDetailResult detail(Long id) { + return BeanUtil.copyProperties(this.getById(id), MenuDetailResult.class); + } + + + public List list(Long pid) { + return this.list(Wrappers.lambdaQuery() + .eq(pid != null, SysMenuEntity::getPid, pid) + .orderByAsc(Arrays.asList(SysMenuEntity::getSort, SysMenuEntity::getId))) + .stream() + .map(it -> BeanUtil.copyProperties(it, MenuDetailResult.class)) + .collect(Collectors.toList()); + } + + + public List listUserMenu(Long userId) { + List sysMenuEntities = baseMapper.selectMenuByUserId(userId); + return sysMenuEntities.stream() + .map(it -> BeanUtil.copyProperties(it, MenuResource.class) + .setMenuCategory(it.getMenuCategory().getVal()) + ) + .collect(Collectors.toList()); + } + + + public PageResult pageList(IPage page, MenuSearchParam menuSearchParam) { + String title = menuSearchParam.getTitle(); + String routeName = menuSearchParam.getRouteName(); + page = this.page(page, Wrappers.lambdaQuery() + .like(StrUtil.isNotBlank(title), SysMenuEntity::getTitle, title) + .like(StrUtil.isNotBlank(routeName), SysMenuEntity::getRouteName, routeName)); + return PageResult.of(page.convert(it -> BeanUtil.copyProperties(it, MenuDetailResult.class))); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/oss/controller/OSSController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/oss/controller/OSSController.java new file mode 100644 index 0000000..5fc494f --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/oss/controller/OSSController.java @@ -0,0 +1,86 @@ +package com.njzscloud.supervisory.oss.controller; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.http.constant.Mime; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mvc.util.FileResponseUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/oss") +public class OSSController { + + @Value("${oss.path}") + private String path; + + @GetMapping("/end_mlt_upload") + public R endMltUpload(String objectName, String uploadId, int partNum) { + // Minio.endMltUpload(objectName, uploadId, partNum); + return R.success(); + } + + @GetMapping("/start_mlt_upload") + public R>> startMltUpload( + String objectName, + String contentType, + int partNum) { + // Minio.startMltUpload(objectName, contentType, partNum) + return R.success(); + } + + @GetMapping("/obtain_presigned_url") + public R> obtainPresignedUrl( + @RequestParam(value = "bucketName") String bucketName, + @RequestParam(value = "filename") String filename) { + String objectName = IdUtil.fastSimpleUUID(); + if (StrUtil.isNotBlank(filename)) { + objectName = objectName + "." + FileUtil.extName(filename); + } + // Map data = Minio.obtainPresignedUrl(bucketName, objectName); + return R.success(); + } + + @GetMapping("/download/{bucketName}/{objectName}") + public void obtainFile( + @PathVariable("bucketName") String bucketName, + @PathVariable("objectName") String objectName, + HttpServletResponse response) throws Exception { + // Tuple2 tuple2 = Minio.obtainFile(bucketName, objectName); + File file = new File(path + "/" + bucketName + "/" + objectName); + if (!file.exists()) { + response.sendError(404); + return; + } + FileInputStream inputStream = new FileInputStream(file); + FileResponseUtil.download(response, inputStream, Mime.BINARY, objectName); + } + + @PostMapping("/upload") + public R upload(@RequestPart("file") MultipartFile file) throws Exception { + String filename = file.getOriginalFilename(); + String objectName = IdUtil.fastSimpleUUID(); + if (StrUtil.isNotBlank(filename)) { + objectName = objectName + "." + FileUtil.extName(filename); + } + InputStream inputStream = file.getInputStream(); + IoUtil.copy(inputStream, Files.newOutputStream(Paths.get(path + "/test/" + objectName))); + inputStream.close(); + + return R.success("download/test/" + objectName); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/cotroller/SysResourceController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/cotroller/SysResourceController.java new file mode 100644 index 0000000..7021790 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/cotroller/SysResourceController.java @@ -0,0 +1,95 @@ +package com.njzscloud.supervisory.resource.cotroller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.supervisory.resource.pojo.SysResourceEntity; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.resource.service.SysResourceService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 系统资源表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_resource") +@RequiredArgsConstructor +public class SysResourceController { + + private final SysResourceService sysResourceService; + + /** + * 新增 + * + * @param sysResourceEntity 数据 + */ + @PostMapping("/add") + public R add(@RequestBody SysResourceEntity sysResourceEntity) { + sysResourceService.add(sysResourceEntity); + return R.success(); + } + + /** + * 修改 + * + * @param sysResourceEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody SysResourceEntity sysResourceEntity) { + sysResourceService.modify(sysResourceEntity); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + sysResourceService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return SysResourceEntity 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysResourceService.detail(id)); + } + + /** + * 分页查询 + * + * @param sysResourceEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysResourceEntity> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SysResourceEntity sysResourceEntity) { + return R.success(sysResourceService.paging(pageParam, sysResourceEntity)); + } + + @GetMapping("/list") + public R> list( + @RequestParam(value = "tableName", required = false) String tableName, + @RequestParam(value = "keywords", required = false) String keywords + ) { + return R.success(sysResourceService.list(Wrappers.lambdaQuery() + .eq(StrUtil.isNotBlank(tableName), SysResourceEntity::getTableName, tableName) + .and(StrUtil.isNotBlank(keywords), ew -> ew.like(SysResourceEntity::getSn, keywords) + .or().like(SysResourceEntity::getMemo, keywords)) + )); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/mapper/SysResourceMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/mapper/SysResourceMapper.java new file mode 100644 index 0000000..4ca407d --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/mapper/SysResourceMapper.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.resource.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.common.security.support.EndpointResource; +import com.njzscloud.common.security.support.MenuResource; +import com.njzscloud.supervisory.resource.pojo.SysResourceEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 系统资源表 + */ +@Mapper +public interface SysResourceMapper extends BaseMapper { + + List listUserMenu(@Param("userId") Long userId); + + List listUserEndpoint(@Param("userId") Long userId); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/pojo/SysResourceEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/pojo/SysResourceEntity.java new file mode 100644 index 0000000..267f616 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/pojo/SysResourceEntity.java @@ -0,0 +1,43 @@ +package com.njzscloud.supervisory.resource.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; + +/** + * 系统资源表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_resource") +public class SysResourceEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 编号 + */ + private String sn; + + /** + * 表名称 + */ + private String tableName; + + /** + * 数据行 Id + */ + private Long dataId; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/service/SysResourceService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/service/SysResourceService.java new file mode 100644 index 0000000..e294431 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/resource/service/SysResourceService.java @@ -0,0 +1,89 @@ +package com.njzscloud.supervisory.resource.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.security.support.EndpointResource; +import com.njzscloud.common.security.support.MenuResource; +import com.njzscloud.common.security.support.Resource; +import com.njzscloud.supervisory.resource.mapper.SysResourceMapper; +import com.njzscloud.supervisory.resource.pojo.SysResourceEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 系统资源表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysResourceService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysResourceEntity 数据 + */ + + public void add(SysResourceEntity sysResourceEntity) { + this.save(sysResourceEntity); + } + + /** + * 修改 + * + * @param sysResourceEntity 数据 + */ + + public void modify(SysResourceEntity sysResourceEntity) { + this.updateById(sysResourceEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysResourceEntity 结果 + */ + + public SysResourceEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysResourceEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysResourceEntity> 分页结果 + */ + + public PageResult paging(PageParam pageParam, SysResourceEntity sysResourceEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.lambdaQuery(sysResourceEntity))); + } + + public Resource selectResourceByUserId(Long userId) { + List menuResources = baseMapper.listUserMenu(userId); + List endpointResource = baseMapper.listUserEndpoint(userId); + return new Resource() + .setMenu(menuResources) + .setEndpoint(endpointResource); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/controller/SysRoleController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/controller/SysRoleController.java new file mode 100644 index 0000000..f79e5de --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/controller/SysRoleController.java @@ -0,0 +1,115 @@ +package com.njzscloud.supervisory.role.controller; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.supervisory.role.service.SysRoleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.njzscloud.supervisory.role.pojo.RoleBindResourceParam; +import com.njzscloud.supervisory.role.pojo.RoleAddParam; +import com.njzscloud.supervisory.role.pojo.RoleModifyParam; +import com.njzscloud.supervisory.role.pojo.RoleQueryParam; +import com.njzscloud.supervisory.menu.pojo.MenuDetailResult; +import com.njzscloud.supervisory.role.pojo.RoleDetailResult; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Set; + +/** + * 角色 + */ +@Slf4j +@RestController +@RequestMapping("/sys_role") +@RequiredArgsConstructor +public class SysRoleController { + + private final SysRoleService sysRoleService; + + /** + * 绑定菜单 + */ + @PostMapping("/bind_menu") + public R bindMenu(@RequestBody RoleBindResourceParam roleBindResourceParam) { + sysRoleService.bindMenu(roleBindResourceParam); + return R.success(); + } + + /** + * 查询角色拥有的菜单 + * + * @param roleId 角色 Id + * @return 菜单列表 + */ + @GetMapping("/owned_menu") + public R> ownedMenu(@RequestParam Long roleId) { + return R.success(sysRoleService.ownedMenu(roleId)); + } + + /** + * 新增 + * + * @param roleAddParam 数据 + * @return Long + */ + @PostMapping("/add") + public R add(@RequestBody @Validated RoleAddParam roleAddParam) { + return R.success(sysRoleService.add(roleAddParam)); + } + + /** + * 修改 + * + * @param roleModifyParam 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody RoleModifyParam roleModifyParam) { + sysRoleService.modify(roleModifyParam); + return R.success(); + } + + /** + * 删除 + * + * @param ids Id + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, "未指定要删除的数据"); + sysRoleService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return RoleDetailResult 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysRoleService.detail(id)); + } + + /** + * 分页查询 + * + * @param roleQueryParam 筛选条件 + * @param page 分页参数 + * @return PageResult<RoleDetailResult> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam page, RoleQueryParam roleQueryParam) { + return R.success(sysRoleService.paging(page.toPage(), roleQueryParam)); + } + + @GetMapping("/user_role") + public R> listUserRole(@RequestParam Long userId) { + return R.success(sysRoleService.listUserRole(userId)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleMapper.java new file mode 100644 index 0000000..9bea9e5 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleMapper.java @@ -0,0 +1,31 @@ +package com.njzscloud.supervisory.role.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.njzscloud.supervisory.role.pojo.SysRoleEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Set; + +/** + *

sys_role

+ *

角色表

+ */ +@Mapper +public interface SysRoleMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页参数 + * @param ew 查询条件 + * @return IPage<SysRoleEntity> 分页结果 + */ + IPage paging(IPage page, @Param(Constants.WRAPPER) Wrapper ew); + + Set selectRoleByUserId(Long userId); + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleResMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleResMapper.java new file mode 100644 index 0000000..364e23e --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/mapper/SysRoleResMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.role.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.role.pojo.SysRoleResourceEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 角色-资源关系表 + */ +@Mapper +public interface SysRoleResMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleAddParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleAddParam.java new file mode 100644 index 0000000..8a7acb1 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleAddParam.java @@ -0,0 +1,32 @@ +package com.njzscloud.supervisory.role.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Getter +@Setter +@ToString +public class RoleAddParam { + /** + * 角色代码 + */ + @NotBlank(message = "角色代码不能为空") + private String roleCode; + + /** + * 角色名称 + */ + @NotBlank(message = "角色名称不能为空") + private String roleName; + + /** + * 备注 + */ + private String memo; + + private List resIds; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleBindResourceParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleBindResourceParam.java new file mode 100644 index 0000000..4672478 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleBindResourceParam.java @@ -0,0 +1,24 @@ +package com.njzscloud.supervisory.role.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +@Getter +@Setter +@Accessors(chain = true) +public class RoleBindResourceParam { + /** + * 角色 Id + */ + private Long roleId; + + /** + * 资源列表 + */ + private List resIds; + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleDetailResult.java new file mode 100644 index 0000000..67783fa --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleDetailResult.java @@ -0,0 +1,40 @@ +package com.njzscloud.supervisory.role.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + *

sys_role

+ *

角色表

+ */ +@Getter +@Setter +@Accessors(chain = true) +public class RoleDetailResult { + + /** + * Id + */ + private Long id; + + /** + * 角色代码; 以 ROLE_ 开头 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + + /** + * 备注 + */ + private String memo; + + private List resIds; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleModifyParam.java new file mode 100644 index 0000000..28f65d7 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleModifyParam.java @@ -0,0 +1,34 @@ +package com.njzscloud.supervisory.role.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +@Setter +@ToString +public class RoleModifyParam { + @NotNull(message = "Id 不能为空") + private Long id; + + /** + * 角色代码 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + + /** + * 备注 + */ + private String memo; + + private List resIds; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleQueryParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleQueryParam.java new file mode 100644 index 0000000..9d9d2e0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/RoleQueryParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.role.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class RoleQueryParam { + /** + * 角色代码 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleEntity.java new file mode 100644 index 0000000..9aa06f1 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleEntity.java @@ -0,0 +1,70 @@ +package com.njzscloud.supervisory.role.pojo; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 角色表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_role") +public class SysRoleEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 角色代码; 以 ROLE_ 开头 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + + /** + * 备注 + */ + private String memo; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleResourceEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleResourceEntity.java new file mode 100644 index 0000000..9599c98 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/pojo/SysRoleResourceEntity.java @@ -0,0 +1,49 @@ +package com.njzscloud.supervisory.role.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; + +/** + * 角色-资源关系表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_role_resource") +public class SysRoleResourceEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 角色 Id; sys_role.id + */ + private Long roleId; + + /** + * 资源 Id; sys_resource.id + */ + private Long resId; + + /** + * 资源编码; sys_resource.sn + */ + private String resSn; + + + /** + * 表名称 + */ + private String tableName; + + /** + * 数据行 Id + */ + private Long dataId; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleResService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleResService.java new file mode 100644 index 0000000..b76cfc0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleResService.java @@ -0,0 +1,77 @@ +package com.njzscloud.supervisory.role.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.role.mapper.SysRoleResMapper; +import com.njzscloud.supervisory.role.pojo.SysRoleResourceEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 角色-资源关系表 + */ +@Slf4j +@Service +public class SysRoleResService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysRoleResourceEntity 数据 + */ + + public void add(SysRoleResourceEntity sysRoleResourceEntity) { + this.save(sysRoleResourceEntity); + } + + /** + * 修改 + * + * @param sysRoleResourceEntity 数据 + */ + + public void modify(SysRoleResourceEntity sysRoleResourceEntity) { + this.updateById(sysRoleResourceEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysRoleResourceEntity 结果 + */ + + public SysRoleResourceEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysRoleResourceEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysRoleResourceEntity> 分页结果 + */ + + public PageResult paging(PageParam pageParam, SysRoleResourceEntity sysRoleResourceEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(sysRoleResourceEntity))); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleService.java new file mode 100644 index 0000000..5d66879 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/role/service/SysRoleService.java @@ -0,0 +1,189 @@ +package com.njzscloud.supervisory.role.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.utils.GroupUtil; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.menu.pojo.MenuDetailResult; +import com.njzscloud.supervisory.menu.service.SysMenuService; +import com.njzscloud.supervisory.resource.pojo.SysResourceEntity; +import com.njzscloud.supervisory.resource.service.SysResourceService; +import com.njzscloud.supervisory.role.mapper.SysRoleMapper; +import com.njzscloud.supervisory.role.pojo.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 角色表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysRoleService extends ServiceImpl implements IService { + + private final SysRoleResService sysRoleResService; + private final SysResourceService sysResourceService; + + private final SysMenuService sysMenuService; + + /** + * 新增 + * + * @param roleAddParam 数据 + * @return Long + */ + + @Transactional(rollbackFor = Exception.class) + public Long add(RoleAddParam roleAddParam) { + SysRoleEntity sysRoleEntity = BeanUtil.copyProperties(roleAddParam, SysRoleEntity.class); + String roleCode = sysRoleEntity.getRoleCode(); + if (!roleCode.startsWith("ROLE_")) { + roleCode = "ROLE_" + roleCode; + sysRoleEntity.setRoleCode(roleCode); + } + boolean exists = this.exists(Wrappers.lambdaQuery().eq(SysRoleEntity::getRoleCode, roleCode)); + Assert.isFalse(exists, "角色编码:{} 已存在", roleCode); + this.save(sysRoleEntity); + Long roleEntityId = sysRoleEntity.getId(); + List resIds = roleAddParam.getResIds(); + if (CollUtil.isNotEmpty(resIds)) bindMenu(new RoleBindResourceParam().setRoleId(roleEntityId).setResIds(resIds)); + return roleEntityId; + } + + /** + * 修改 + * + * @param roleModifyParam 数据 + */ + + @Transactional(rollbackFor = Exception.class) + public void modify(RoleModifyParam roleModifyParam) { + SysRoleEntity sysRoleEntity = BeanUtil.copyProperties(roleModifyParam, SysRoleEntity.class); + this.updateById(sysRoleEntity); + List resIds = roleModifyParam.getResIds(); + if (CollUtil.isNotEmpty(resIds)) + bindMenu(new RoleBindResourceParam().setRoleId(roleModifyParam.getId()).setResIds(resIds)); + } + + /** + * 删除 + * + * @param ids Id + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return RoleDetailResult 结果 + */ + + public RoleDetailResult detail(Long id) { + SysRoleEntity sysRoleEntity = this.getById(id); + List list = sysRoleResService.list(Wrappers.lambdaQuery().eq(SysRoleResourceEntity::getRoleId, id)); + List resIds = list.stream().map(SysRoleResourceEntity::getResId).collect(Collectors.toList()); + return BeanUtil.copyProperties(sysRoleEntity, RoleDetailResult.class) + .setResIds(resIds); + } + + /** + * 分页查询 + * + * @param page 分页参数 + * @param roleQueryParam 筛选条件 + * @return PageResult<RoleDetailResult> 分页结果 + */ + + public PageResult paging(IPage page, RoleQueryParam roleQueryParam) { + String roleCode = roleQueryParam.getRoleCode(); + String roleName = roleQueryParam.getRoleName(); + Wrapper ew = Wrappers.lambdaQuery() + .like(roleCode != null, SysRoleEntity::getRoleCode, roleCode) + .like(roleName != null, SysRoleEntity::getRoleName, roleName); + return PageResult.of(baseMapper.paging(page, ew).convert(it -> BeanUtil.copyProperties(it, RoleDetailResult.class))); + } + + /** + * 绑定菜单 + */ + + @Transactional(rollbackFor = Exception.class) + public void bindMenu(RoleBindResourceParam roleBindResourceParam) { + Long roleId = roleBindResourceParam.getRoleId(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(SysRoleEntity::getId, roleId)); + Assert.isTrue(exists, () -> Exceptions.exception("角色不存在")); + List resIds = roleBindResourceParam.getResIds(); + + List sysMenuEntities = sysResourceService.listByIds(resIds); + Assert.isTrue(sysMenuEntities.size() == resIds.size(), () -> Exceptions.exception("资源不存在")); + + List oldResIds = sysRoleResService.list(Wrappers.lambdaQuery().eq(SysRoleResourceEntity::getRoleId, roleId)) + .stream().map(SysRoleResourceEntity::getResId).collect(Collectors.toList()); + + + Collection delResIds = CollUtil.subtract(oldResIds, resIds); + if (CollUtil.isNotEmpty(delResIds)) { + sysRoleResService.remove(Wrappers.lambdaQuery().eq(SysRoleResourceEntity::getRoleId, roleId).in(SysRoleResourceEntity::getResId, delResIds)); + } + Map map = GroupUtil.k_o(sysMenuEntities, SysResourceEntity::getId); + List addRelations = CollUtil.subtract(resIds, oldResIds) + .stream().map(it -> { + SysResourceEntity sysResourceEntity = map.get(it); + return new SysRoleResourceEntity() + .setRoleId(roleId) + .setResId(it) + .setResSn(sysResourceEntity.getSn()) + .setTableName(sysResourceEntity.getTableName()) + .setDataId(sysResourceEntity.getDataId()); + }) + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(addRelations)) return; + sysRoleResService.saveBatch(addRelations); + } + + /** + * 查询角色拥有的菜单 + * + * @param roleId 角色 Id + * @return 菜单列表 + */ + + public List ownedMenu(Long roleId) { + List sysRoleResourceEntities = sysRoleResService.list(Wrappers.lambdaQuery() + .eq(SysRoleResourceEntity::getRoleId, roleId) + .eq(SysRoleResourceEntity::getTableName, "sys_menu") + ); + if (CollUtil.isNotEmpty(sysRoleResourceEntities)) { + List menuIds = sysRoleResourceEntities.stream().map(SysRoleResourceEntity::getResId).collect(Collectors.toList()); + return sysMenuService.listByIds(menuIds) + .stream() + .map(it -> BeanUtil.copyProperties(it, MenuDetailResult.class)) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + + public Set listUserRole(Long userId) { + return baseMapper.selectRoleByUserId(userId); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/contant/Gender.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/contant/Gender.java new file mode 100644 index 0000000..37587c2 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/contant/Gender.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.user.contant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import com.njzscloud.common.core.ienum.DictStr; + +/** + * 字典代码:gender + * 字典名称:性别 + */ +@Getter +@RequiredArgsConstructor +public enum Gender implements DictStr { + Unknown("Unknown", "未知"), + Man("Man", "男"), + Woman("Woman", "女"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/controller/SysUserController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/controller/SysUserController.java new file mode 100644 index 0000000..67d1201 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/controller/SysUserController.java @@ -0,0 +1,106 @@ +package com.njzscloud.supervisory.user.controller; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.user.pojo.*; +import com.njzscloud.supervisory.user.service.SysUserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + *

sys_user

+ *

用户信息表

+ */ +@Slf4j +@RestController +@RequestMapping("/sys_user") +@RequiredArgsConstructor +public class SysUserController { + + private final SysUserService sysUserService; + + /** + * 新增 + * + * @param addUserParam 数据 + * @return boolean 结果; true-->成功、false-->失败 + */ + @PostMapping("/add") + public R add(@RequestBody @Validated AddUserParam addUserParam) { + return R.success(sysUserService.add(addUserParam)); + } + + /** + * 修改 + * + * @param userModifyParam 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody UserModifyParam userModifyParam) { + sysUserService.modify(userModifyParam); + return R.success(); + } + + /** + * 删除 + * + * @param ids Ids + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, "未指定要删除的数据"); + sysUserService.del(ids); + return R.success(); + } + + /** + * 详情 + * + * @param id Id + * @return UserDetailResult 结果 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(sysUserService.detail(id)); + } + + @GetMapping("/my") + public R my() { + return R.success(sysUserService.my()); + } + + /** + * 分页查询 + * + * @param userQueryParam 筛选条件 + * @param page 分页参数 + * @return PageResult<UserDetailResult> 分页结果 + */ + @GetMapping("/paging") + public R> paging(PageParam page, UserQueryParam userQueryParam) { + return R.success(sysUserService.paging(page.toPage(), userQueryParam)); + } + + /** + * 用户注册 + * + * @param userRegisterParam 参数 + */ + @PostMapping("/register") + public R register(@RequestBody @Validated UserRegisterParam userRegisterParam) { + sysUserService.register(userRegisterParam); + return R.success(); + } + + @PostMapping("/modify_passwd") + public R modifyPasswd(@RequestBody ModifyPasswdParam modifyPasswdParam) { + sysUserService.modifyPasswd(modifyPasswdParam); + return R.success(); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserAccountMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserAccountMapper.java new file mode 100644 index 0000000..64d940a --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserAccountMapper.java @@ -0,0 +1,29 @@ +package com.njzscloud.supervisory.user.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.njzscloud.supervisory.user.pojo.SysUserAccountEntity; +import com.njzscloud.common.security.support.UserDetail; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + *

sys_user_account

+ *

用户账号信息表

+ */ +@Mapper +public interface SysUserAccountMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页参数 + * @param ew 查询条件 + * @return IPage<SysUserAccountEntity> 分页结果 + */ + IPage paging(IPage page, @Param(Constants.WRAPPER) Wrapper ew); + + UserDetail selectUserByAccount(String account); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserMapper.java new file mode 100644 index 0000000..68d1455 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserMapper.java @@ -0,0 +1,26 @@ +package com.njzscloud.supervisory.user.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.njzscloud.supervisory.user.pojo.UserDetailResult; +import com.njzscloud.supervisory.user.pojo.SysUserEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 用户信息表 + */ +@Mapper +public interface SysUserMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页参数 + * @param ew 查询条件 + * @return IPage<UserDetailResult> 分页结果 + */ + IPage paging(IPage page, @Param(Constants.WRAPPER) Wrapper ew); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserRoleMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..26bc2a8 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/mapper/SysUserRoleMapper.java @@ -0,0 +1,18 @@ +package com.njzscloud.supervisory.user.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.user.pojo.SysUserRoleEntity; +import com.njzscloud.supervisory.role.pojo.RoleDetailResult; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户-角色关系表 + */ +@Mapper +public interface SysUserRoleMapper extends BaseMapper { + List listRole(@Param("ew") QueryWrapper ew); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserAccountParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserAccountParam.java new file mode 100644 index 0000000..c2cf5aa --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserAccountParam.java @@ -0,0 +1,68 @@ +package com.njzscloud.supervisory.user.pojo; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; + +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class AddUserAccountParam implements Constrained { + /** + * 用户 Id; sys_user.id + */ + private Long userId; + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + private String username; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + private String secret; + /** + * 微信 openid + */ + private String wechatOpenid; + /** + * 微信 unionid + */ + private String wechatUnionid; + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + // ValidRule.of(() -> StrUtil.isNotBlank(username), "用户名不能为空"), + // ValidRule.of(() -> StrUtil.isNotBlank(secret), "密码不能为空"), + ValidRule.of(() -> (StrUtil.isNotBlank(wechatOpenid) && StrUtil.isNotBlank(wechatUnionid)) || + (StrUtil.isBlank(wechatOpenid) && StrUtil.isBlank(wechatUnionid)), "微信账号不正确"), + }; + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserParam.java new file mode 100644 index 0000000..f7ff49e --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/AddUserParam.java @@ -0,0 +1,48 @@ +package com.njzscloud.supervisory.user.pojo; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.supervisory.user.contant.Gender; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import javax.validation.Valid; +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class AddUserParam implements Constrained { + + + /** + * 昵称 + */ + private String nickname; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + /** + * 账号信息 + */ + @Valid + private AddUserAccountParam account; + /** + * 角色 Id 列表 + */ + private List roleIds; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(nickname), "用户昵称不能为空"), + ValidRule.of(() -> account != null, "账号信息不能为空"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyInfoParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyInfoParam.java new file mode 100644 index 0000000..3d57f30 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyInfoParam.java @@ -0,0 +1,23 @@ +package com.njzscloud.supervisory.user.pojo; + +import com.njzscloud.supervisory.user.contant.Gender; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ModifyInfoParam { + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + private Gender gender; + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyPasswdParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyPasswdParam.java new file mode 100644 index 0000000..41fc3b0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ModifyPasswdParam.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.user.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Setter +@Accessors(chain = true) +public class ModifyPasswdParam { + private String oldPasswd; + private String newPasswd; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/MyResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/MyResult.java new file mode 100644 index 0000000..277982c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/MyResult.java @@ -0,0 +1,51 @@ +package com.njzscloud.supervisory.user.pojo; + +import com.njzscloud.common.security.support.MenuResource; +import com.njzscloud.supervisory.user.contant.Gender; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class MyResult { + /** + * Id + */ + private Long id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + List menus; + + List> setting; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ObtainInfoResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ObtainInfoResult.java new file mode 100644 index 0000000..09e602f --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/ObtainInfoResult.java @@ -0,0 +1,94 @@ +package com.njzscloud.supervisory.user.pojo; + +import cn.hutool.setting.Setting; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +@Getter +@Setter +@Accessors(chain = true) +public class ObtainInfoResult { + /** + * 用户 Id + */ + private Long id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + /** + * 菜单 + */ + private List menus; + + private Setting setting; + + private Long tenantId; + + private String tenantName; + + + @Getter + @Setter + @Accessors(chain = true) + public static class Menu { + + private String sn; + /** + * Id + */ + private Long id; + + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + private Integer tier; + + /** + * 面包路径; 逗号分隔 + */ + private String[] breadcrumb; + + /** + * 类型; 字典代码:menu_category + */ + private String menuCategory; + + /** + * 标签是否冻结; 0-->否、1-->是 + */ + private Boolean freeze; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserAccountEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserAccountEntity.java new file mode 100644 index 0000000..d2adc2f --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserAccountEntity.java @@ -0,0 +1,108 @@ +package com.njzscloud.supervisory.user.pojo; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + *

sys_user_account

+ *

用户账号信息表

+ */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_user_account") +public class SysUserAccountEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 Id; sys_user.id + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 密码 + */ + private String secret; + + /** + * 微信 openid + */ + private String wechatOpenid; + + /** + * 微信 unionid + */ + private String wechatUnionid; + + /** + * 注册时间 + */ + private LocalDateTime regdate; + + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserEntity.java new file mode 100644 index 0000000..596259f --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserEntity.java @@ -0,0 +1,83 @@ +package com.njzscloud.supervisory.user.pojo; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import com.njzscloud.supervisory.user.contant.Gender; + +import java.time.LocalDateTime; + +/** + * 用户信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_user") +public class SysUserEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserRoleEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserRoleEntity.java new file mode 100644 index 0000000..89287d0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/SysUserRoleEntity.java @@ -0,0 +1,35 @@ +package com.njzscloud.supervisory.user.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + *

用户-角色关系表

+ */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_user_role") +public class SysUserRoleEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 Id; sys_user.id + */ + private Long userId; + + /** + * 角色 Id; sys_role.id + */ + private Long roleId; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountDetailResult.java new file mode 100644 index 0000000..a43f045 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountDetailResult.java @@ -0,0 +1,43 @@ +package com.njzscloud.supervisory.user.pojo; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserAccountDetailResult { + private Long id; + + /** + * 用户 Id; sys_user.id + */ + private Long userId; + /** + * 用户名 + */ + private String username; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 微信 openid + */ + private String wechatOpenid; + /** + * 微信 unionid + */ + private String wechatUnionid; + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountModifyParam.java new file mode 100644 index 0000000..86268da --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserAccountModifyParam.java @@ -0,0 +1,68 @@ +package com.njzscloud.supervisory.user.pojo; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; + +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class UserAccountModifyParam implements Constrained { + /** + * 用户 Id; sys_user.id + */ + private Long userId; + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + private String username; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + private String secret; + /** + * 微信 openid + */ + private String wechatOpenid; + /** + * 微信 unionid + */ + private String wechatUnionid; + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + // ValidRule.of(() -> StrUtil.isNotBlank(username), "用户名不能为空"), + // ValidRule.of(() -> StrUtil.isNotBlank(secret), "密码不能为空"), + ValidRule.of(() -> (StrUtil.isNotBlank(wechatOpenid) && StrUtil.isNotBlank(wechatUnionid)) || + (StrUtil.isBlank(wechatOpenid) && StrUtil.isBlank(wechatUnionid)), "微信账号不正确"), + }; + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserDetailResult.java new file mode 100644 index 0000000..09fbdc0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserDetailResult.java @@ -0,0 +1,49 @@ +package com.njzscloud.supervisory.user.pojo; + +import com.njzscloud.supervisory.role.pojo.RoleDetailResult; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.njzscloud.supervisory.user.contant.Gender; + +import java.util.List; + +@Getter +@Setter +@Accessors(chain = true) +public class UserDetailResult { + private Long id; + + /** + * + */ + private String companyName; + + /** + * 昵称 + */ + private String nickname; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 账号信息 + */ + private UserAccountDetailResult account; + /** + * 角色列表 + */ + private List roles; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserModifyParam.java new file mode 100644 index 0000000..f0d0a38 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserModifyParam.java @@ -0,0 +1,45 @@ +package com.njzscloud.supervisory.user.pojo; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.supervisory.user.contant.Gender; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.util.List; + + +@Getter +@Setter +@Constraint +public class UserModifyParam implements Constrained { + @NotNull + private Long id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + private UserAccountModifyParam account; + + /** + * 角色 Id 列表 + */ + private List roleIds; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> !(StrUtil.isBlank(nickname) && gender != null), "昵称和性别至少一个有值"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserQueryParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserQueryParam.java new file mode 100644 index 0000000..0a7e05b --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserQueryParam.java @@ -0,0 +1,37 @@ +package com.njzscloud.supervisory.user.pojo; + +import lombok.Getter; +import lombok.Setter; +import com.njzscloud.supervisory.user.contant.Gender; + +@Getter +@Setter +public class UserQueryParam { + /** + * 昵称 + */ + private String nickname; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + /** + * 用户名 + */ + private String username; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserRegisterParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserRegisterParam.java new file mode 100644 index 0000000..c8a52fa --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/pojo/UserRegisterParam.java @@ -0,0 +1,201 @@ +package com.njzscloud.supervisory.user.pojo; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.supervisory.user.contant.Gender; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class UserRegisterParam implements Constrained { + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 账号信息 + */ + private Account account; + /** + * 公司信息 + */ + private Company company; + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(nickname), "用户昵称不能为空"), + ValidRule.of(() -> account != null, "账号信息不能为空"), + }; + } + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class Account implements Constrained { + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String secret; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(username), "用户名不能为空"), + ValidRule.of(() -> StrUtil.isNotBlank(secret), "密码不能为空"), + }; + } + } + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class Company implements Constrained { + /** + * 企业名称 + */ + private String companyName; + + /** + * 统一社会信用代码; biz_company.uscc + */ + private String uscc; + + /** + * 营业执照; 图片 + */ + private String businessLicense; + + /** + * 资质证明; 图片 + */ + private String certification; + + /** + * 营业执照有效期; [开始日期,结束日期] + */ + private List businessLicenseDate; + + /** + * 资质证明有效期; [开始日期,结束日期] + */ + private List certificationDate; + + /** + * 法人名称 + */ + private String legalRepresentative; + + /** + * 省; 名称 + */ + private String province; + + /** + * 市; 名称 + */ + private String city; + + /** + * 区; 名称 + */ + private String county; + + /** + * 详细地址 + */ + private String address; + + /** + * 联系人 + */ + private String contacts; + + /** + * 联系电话 + */ + private String phoneNum; + + /** + * 服务范围 + */ + private List scopeList; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{}; + } + } + + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class Scope implements Constrained { + /** + * 省; 名称 + */ + private String province; + + /** + * 市; 名称 + */ + private String city; + + /** + * 区; 名称 + */ + private String county; + + /** + * 街道; 名称 + */ + private String street; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{}; + } + } + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserAccountService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserAccountService.java new file mode 100644 index 0000000..a373ecc --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserAccountService.java @@ -0,0 +1,101 @@ +package com.njzscloud.supervisory.user.service; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.EncryptUtil; +import com.njzscloud.common.security.util.SecurityUtil; +import com.njzscloud.supervisory.user.mapper.SysUserAccountMapper; +import com.njzscloud.supervisory.user.pojo.AddUserAccountParam; +import com.njzscloud.supervisory.user.pojo.SysUserAccountEntity; +import com.njzscloud.supervisory.user.pojo.UserAccountModifyParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + *

sys_user_account

+ *

用户账号信息表

+ */ +@Slf4j +@Service +public class SysUserAccountService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param addUserAccountParam 数据 + * @return boolean 结果; true-->成功、false-->失败 + */ + public void add(AddUserAccountParam addUserAccountParam) { + SysUserAccountEntity userAccountEntity = BeanUtil.copyProperties(addUserAccountParam, SysUserAccountEntity.class) + .setSecret(EncryptUtil.encrypt(addUserAccountParam.getSecret())) + .setRegdate(LocalDateTime.now()); + this.save(userAccountEntity); + } + + /** + * 修改 + * + * @param userAccountModifyParam 数据 + * @return boolean 结果; true-->成功、false-->失败 + */ + @Transactional(rollbackFor = Exception.class) + public boolean modify(UserAccountModifyParam userAccountModifyParam) { + SysUserAccountEntity oldData = this.getOne(Wrappers.lambdaQuery().eq(SysUserAccountEntity::getUserId, userAccountModifyParam.getUserId())); + SysUserAccountEntity entity = BeanUtil.copyProperties(userAccountModifyParam, SysUserAccountEntity.class) + .setId(oldData.getId()) + .setSecret(EncryptUtil.encrypt(userAccountModifyParam.getSecret())); + if (userAccountModifyParam.getDisabled() == Boolean.TRUE) { + Long userId = oldData.getUserId(); + SecurityUtil.removeToken(userId); + } + return this.updateById(entity); + } + + /** + * 删除 + * + * @param id Id + * @return boolean 结果; true-->成功、false-->失败 + */ + + public boolean del(Long id) { + return this.removeById(id); + } + + /** + * 详情 + * + * @param id Id + * @return SysUserAccountEntity 结果 + */ + + public SysUserAccountEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param sysUserAccountEntity 筛选条件 + * @param page 分页参数 + * @return IPage<SysUserAccountEntity> 分页结果 + */ + + public IPage paging(IPage page, SysUserAccountEntity sysUserAccountEntity) { + Wrapper ew = Wrappers.query(sysUserAccountEntity); + return baseMapper.paging(page, ew); + } + + public UserDetail getUserInfo(String account) { + return baseMapper.selectUserByAccount(account); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserRoleService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserRoleService.java new file mode 100644 index 0000000..841a2b6 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserRoleService.java @@ -0,0 +1,76 @@ +package com.njzscloud.supervisory.user.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.supervisory.role.pojo.RoleDetailResult; +import com.njzscloud.supervisory.user.pojo.SysUserRoleEntity; +import com.njzscloud.supervisory.user.mapper.SysUserRoleMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

用户-角色关系表

+ */ +@Slf4j +@Service +public class SysUserRoleService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param sysUserRoleEntity 数据 + * @return boolean 结果; true-->成功、false-->失败 + */ + + public boolean add(SysUserRoleEntity sysUserRoleEntity) { + return this.save(sysUserRoleEntity); + } + + /** + * 修改 + * + * @param sysUserRoleEntity 数据 + * @return boolean 结果; true-->成功、false-->失败 + */ + + public boolean modify(SysUserRoleEntity sysUserRoleEntity) { + return this.updateById(sysUserRoleEntity); + } + + /** + * 删除 + * + * @param id Id + * @return boolean 结果; true-->成功、false-->失败 + */ + + public boolean del(Long id) { + return this.removeById(id); + } + + /** + * 详情 + * + * @param id Id + * @return SysUserRoleEntity 结果 + */ + + public SysUserRoleEntity detail(Long id) { + return this.getById(id); + } + + + public void delByUserIds(List userIdList) { + this.remove(Wrappers.lambdaQuery().in(SysUserRoleEntity::getUserId, userIdList)); + } + + + public List listRole(Long userId) { + return baseMapper.listRole(Wrappers.query().eq("a.user_id", userId)); + } + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserService.java new file mode 100644 index 0000000..1684e02 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/user/service/SysUserService.java @@ -0,0 +1,236 @@ +package com.njzscloud.supervisory.user.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.security.support.MenuResource; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.EncryptUtil; +import com.njzscloud.common.security.util.SecurityUtil; +import com.njzscloud.supervisory.menu.service.SysMenuService; +import com.njzscloud.supervisory.role.pojo.RoleDetailResult; +import com.njzscloud.supervisory.role.pojo.SysRoleEntity; +import com.njzscloud.supervisory.role.service.SysRoleService; +import com.njzscloud.supervisory.user.contant.Gender; +import com.njzscloud.supervisory.user.mapper.SysUserMapper; +import com.njzscloud.supervisory.user.pojo.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户信息表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysUserService extends ServiceImpl implements IService { + private final SysUserAccountService sysUserAccountService; + private final SysRoleService sysRoleService; + private final SysUserRoleService sysUserRoleService; + private final SysMenuService sysMenuService; + + + /** + * 新增 + * + * @param addUserParam 数据 + * @return boolean 结果; true-->成功、false-->失败 + */ + @Transactional(rollbackFor = Exception.class) + public Long add(AddUserParam addUserParam) { + AddUserAccountParam addUserAccountParam = addUserParam.getAccount(); + String username = addUserAccountParam.getUsername(); + String email = addUserAccountParam.getEmail(); + String phone = addUserAccountParam.getPhone(); + String wechatOpenid = addUserAccountParam.getWechatOpenid(); + String wechatUnionid = addUserAccountParam.getWechatUnionid(); + List oldSysUserList = sysUserAccountService.list(Wrappers.lambdaQuery() + .eq(SysUserAccountEntity::getUsername, username) + .or().eq(StrUtil.isNotBlank(email), SysUserAccountEntity::getEmail, email) + .or().eq(StrUtil.isNotBlank(phone), SysUserAccountEntity::getPhone, phone) + .or(StrUtil.isNotBlank(wechatOpenid) && StrUtil.isNotBlank(wechatUnionid), it -> it.eq(SysUserAccountEntity::getWechatOpenid, wechatOpenid).eq(SysUserAccountEntity::getWechatUnionid, wechatUnionid)) + ); + Assert.isTrue(oldSysUserList.stream().noneMatch(it -> username.equals(it.getUsername())), () -> Exceptions.exception("用户名【{}】已被使用", username)); + Assert.isTrue(StrUtil.isBlank(email) || oldSysUserList.stream().noneMatch(it -> email.equals(it.getEmail())), () -> Exceptions.exception("邮箱【{}】已被使用", email)); + Assert.isTrue(StrUtil.isBlank(phone) || oldSysUserList.stream().noneMatch(it -> phone.equals(it.getPhone())), () -> Exceptions.exception("手机号【{}】已被使用", phone)); + Assert.isTrue(StrUtil.isBlank(wechatOpenid) || StrUtil.isBlank(wechatUnionid) || oldSysUserList.stream().noneMatch(it -> wechatOpenid.equals(it.getWechatOpenid()) && wechatUnionid.equals(it.getWechatUnionid())), () -> Exceptions.exception("该微信账号已被使用")); + + SysUserEntity sysUserEntity = BeanUtil.copyProperties(addUserParam, SysUserEntity.class); + if (StrUtil.isNotBlank(email)) sysUserEntity.setEmail(email); + if (StrUtil.isNotBlank(phone)) sysUserEntity.setPhone(phone); + this.save(sysUserEntity); + + Long userEntityId = sysUserEntity.getId(); + + sysUserAccountService.add(addUserAccountParam.setUserId(userEntityId)); + + List roleIds = addUserParam.getRoleIds(); + if (CollUtil.isEmpty(roleIds)) return userEntityId; + + List sysRoleEntities = sysRoleService.listByIds(roleIds); + Assert.isTrue(sysRoleEntities.size() == roleIds.size(), () -> Exceptions.exception("角色不存在")); + + sysUserRoleService.saveBatch(roleIds.stream().map(roleId -> new SysUserRoleEntity().setRoleId(roleId).setUserId(userEntityId)).collect(Collectors.toList())); + + return userEntityId; + } + + /** + * 修改 + * + * @param userModifyParam 数据 + */ + public void modify(UserModifyParam userModifyParam) { + Long id = userModifyParam.getId(); + SysUserEntity sysUserEntity = this.getById(id); + Assert.notNull(sysUserEntity, "要修改的数据不存在"); + + UserAccountModifyParam account = userModifyParam.getAccount(); + String username = account.getUsername(); + String email = account.getEmail(); + String phone = account.getPhone(); + String wechatOpenid = account.getWechatOpenid(); + String wechatUnionid = account.getWechatUnionid(); + + List oldSysUserList = sysUserAccountService.list(Wrappers.lambdaQuery() + .eq(SysUserAccountEntity::getUsername, username) + .or().eq(StrUtil.isNotBlank(email), SysUserAccountEntity::getEmail, email) + .or().eq(StrUtil.isNotBlank(phone), SysUserAccountEntity::getPhone, phone) + .or(StrUtil.isNotBlank(wechatOpenid) && StrUtil.isNotBlank(wechatUnionid), it -> it.eq(SysUserAccountEntity::getWechatOpenid, wechatOpenid).eq(SysUserAccountEntity::getWechatUnionid, wechatUnionid)) + ); + Assert.isTrue(oldSysUserList.stream().noneMatch(it -> username.equals(it.getUsername())), () -> Exceptions.exception("用户名【{}】已被使用", username)); + Assert.isTrue(StrUtil.isBlank(email) || oldSysUserList.stream().noneMatch(it -> email.equals(it.getEmail())), () -> Exceptions.exception("邮箱【{}】已被使用", email)); + Assert.isTrue(StrUtil.isBlank(phone) || oldSysUserList.stream().noneMatch(it -> phone.equals(it.getPhone())), () -> Exceptions.exception("手机号【{}】已被使用", phone)); + Assert.isTrue(StrUtil.isBlank(wechatOpenid) || StrUtil.isBlank(wechatUnionid) || oldSysUserList.stream().noneMatch(it -> wechatOpenid.equals(it.getWechatOpenid()) && wechatUnionid.equals(it.getWechatUnionid())), () -> Exceptions.exception("该微信账号已被使用")); + + sysUserEntity = BeanUtil.copyProperties(userModifyParam, SysUserEntity.class); + if (StrUtil.isNotBlank(email)) sysUserEntity.setEmail(email); + if (StrUtil.isNotBlank(phone)) sysUserEntity.setPhone(phone); + this.updateById(sysUserEntity); + + sysUserAccountService.modify(account.setUserId(id)); + + List roleIds = userModifyParam.getRoleIds(); + if (CollUtil.isEmpty(roleIds)) return; + + List sysRoleEntities = sysRoleService.listByIds(roleIds); + Assert.isTrue(sysRoleEntities.size() == roleIds.size(), () -> Exceptions.exception("角色不存在")); + + sysUserRoleService.remove(Wrappers.lambdaQuery().eq(SysUserRoleEntity::getUserId, id)); + + sysUserRoleService.saveBatch(roleIds.stream().map(roleId -> new SysUserRoleEntity().setRoleId(roleId).setUserId(id)).collect(Collectors.toList())); + } + + /** + * 删除 + * + * @param ids Id + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeByIds(ids); + sysUserAccountService.remove(Wrappers.lambdaQuery().in(SysUserAccountEntity::getUserId, ids)); + sysUserRoleService.delByUserIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return UserDetailResult 结果 + */ + public UserDetailResult detail(Long id) { + SysUserEntity sysUserEntity = this.getById(id); + Assert.notNull(sysUserEntity, "未查询到用户信息"); + UserDetailResult userDetailResult = BeanUtil.copyProperties(sysUserEntity, UserDetailResult.class); + SysUserAccountEntity userAccountEntity = sysUserAccountService.getOne(Wrappers.lambdaQuery().eq(SysUserAccountEntity::getUserId, id)); + Assert.notNull(userAccountEntity, "未查询到用户信息"); + UserAccountDetailResult userAccountDetailResult = BeanUtil.copyProperties(userAccountEntity, UserAccountDetailResult.class); + List roleDetailResults = sysUserRoleService.listRole(id); + return userDetailResult + .setAccount(userAccountDetailResult) + .setRoles(roleDetailResults); + } + + /** + * 分页查询 + * + * @param page 分页参数 + * @param userQueryParam 筛选条件 + * @return PageResult<UserDetailResult> 分页结果 + */ + public PageResult paging(IPage page, UserQueryParam userQueryParam) { + String nickname = userQueryParam.getNickname(); + Gender gender = userQueryParam.getGender(); + String email = userQueryParam.getEmail(); + String phone = userQueryParam.getPhone(); + String username = userQueryParam.getUsername(); + Boolean disabled = userQueryParam.getDisabled(); + QueryWrapper ew = Wrappers.query() + .like(StrUtil.isNotBlank(nickname), "a.nickname", nickname) + .and(gender != null, it -> it.eq("a.gender", gender.getVal())) + .like(StrUtil.isNotBlank(email), "a.email", email) + .like(StrUtil.isNotBlank(phone), "a.phone", phone) + .like(StrUtil.isNotBlank(username), "b.username", username) + .eq(disabled != null, "b.disabled", disabled); + return PageResult.of(baseMapper.paging(page, ew)); + } + + public UserDetail getUserInfo(String account) { + return sysUserAccountService.getUserInfo(account); + } + + public void modifyPasswd(ModifyPasswdParam modifyPasswdParam) { + Long userId = SecurityUtil.loginUser().getUserId(); + SysUserAccountEntity accountEntity = sysUserAccountService.getOne(Wrappers.lambdaQuery().eq(SysUserAccountEntity::getUserId, userId)); + boolean matches = EncryptUtil.matches(modifyPasswdParam.getOldPasswd(), accountEntity.getSecret()); + Assert.isTrue(matches, () -> Exceptions.exception("密码错误")); + String newPasswd = EncryptUtil.encrypt(modifyPasswdParam.getNewPasswd()); + + Long id = accountEntity.getId(); + + sysUserAccountService.updateById(new SysUserAccountEntity() + .setId(id) + .setSecret(newPasswd) + ); + } + + /** + * 用户注册 + * + * @param userRegisterParam 参数 + */ + @Transactional(rollbackFor = Exception.class) + public void register(UserRegisterParam userRegisterParam) { + AddUserParam addUserParam = BeanUtil.copyProperties(userRegisterParam, AddUserParam.class); + addUserParam.setAccount(BeanUtil.copyProperties(userRegisterParam.getAccount(), AddUserAccountParam.class)); + Long userId = this.add(addUserParam); + + UserRegisterParam.Company company = userRegisterParam.getCompany(); + Assert.notNull(company, "公司信息不能为空"); + + List scopeList = company.getScopeList(); + } + + public MyResult my() { + UserDetail userDetail = SecurityUtil.loginUser(); + Long userId = userDetail.getUserId(); + SysUserEntity sysUserEntity = this.getById(userId); + List menuResources = sysMenuService.listUserMenu(userId); + return BeanUtil.copyProperties(sysUserEntity, MyResult.class) + .setMenus(menuResources); + } +} diff --git a/njzscloud-svr/src/main/resources/application-dev.yml b/njzscloud-svr/src/main/resources/application-dev.yml new file mode 100644 index 0000000..d4a4a90 --- /dev/null +++ b/njzscloud-svr/src/main/resources/application-dev.yml @@ -0,0 +1,49 @@ +spring: + servlet: + multipart: + location: D:\ProJects\gov_manage\njzscloud-supervisory-svr\logs\temp + datasource: + url: jdbc:mysql://localhost:3306/zsy-recycling-supervision?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: root + + redis: + enable: false + pubsub: false + host: localhost + #password: redis + database: 0 + port: 6379 + + mail: + # 邮件服务器 + host: smtp.qq.com + # 发送邮件的账户 + username: lzq@qq.com + # 授权码 + password: lzq +oss: + path: D:/ProJects/gov_manage/njzscloud-supervisory-svr/logs + minio: + endpoint: http://localhost:9000 + access-key: minioadmin + secret-key: minioadmin + bucket-name: test + +app: + district: + province: 320000 + city: 320100 + +mybatis-plus: + tunnel: + enable: false + ssh: + host: 139.224.54.144 + port: 22 + user: root + credentials: D:/我的/再昇云/服务器秘钥/139.224.54.144_YZS_S1.pem + localPort: 33061 + db: + host: localhost + port: 33061 diff --git a/njzscloud-svr/src/main/resources/application-prod.yml b/njzscloud-svr/src/main/resources/application-prod.yml new file mode 100644 index 0000000..c1e174d --- /dev/null +++ b/njzscloud-svr/src/main/resources/application-prod.yml @@ -0,0 +1,31 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/njzscloud_supervisory?characterEncoding=UTF-8&allowMultiQueries=true&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + security: + auth-ignores: + - /index.html + - /jquery.js + - /sockjs.js + - /stomp.js + - /sockjs.js.map + - /favicon.ico + # - /ws/** + + mail: + # 邮件服务器 + host: smtp.qq.com + # 发送邮件的账户 + username: lzq@qq.com + # 授权码 + password: lzq + + redis: + enable: false + pubsub: false + host: 192.168.1.17 + #password: redis + database: 0 + port: 6379 + diff --git a/njzscloud-svr/src/main/resources/application.yml b/njzscloud-svr/src/main/resources/application.yml new file mode 100644 index 0000000..06c40c3 --- /dev/null +++ b/njzscloud-svr/src/main/resources/application.yml @@ -0,0 +1,74 @@ +server: + tomcat: + max-http-form-post-size: 20MB + port: 10086 +spring: + servlet: + multipart: + max-file-size: 20MB + max-request-size: 20MB + file-size-threshold: 1MB + resolve-lazily: true + profiles: + active: ${APP_PROFILE:dev} + web: + resources: + add-mappings: false + mvc: + throw-exception-if-no-handler-found: true + format: + date: yyyy-MM-dd + time: HH:mm:ss + date-time: yyyy-MM-dd HH:mm:ss + jackson: + locale: zh_CN + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + default-property-inclusion: always + serialization: + # 对象不含任何字段时是否报错 + fail-on-empty-beans: false + # 是否捕获并且包装异常信息 + wrap-exceptions: true + # 是否将字符数组输出为数组 + write-char-arrays-as-json-arrays: true + # 格式化输出(加入空格/回车) + indent-output: true + # 对 Map 类型按键排序 + order-map-entries-by-keys: true + # 是否将日期/时间序列化为时间错 + write-dates-as-timestamps: false + write-bigdecimal-as-plain: true + deserialization: + # 未知属性是否报错 + fail-on-unknown-properties: false + generator: + # 是否忽略位置属性 + ignore-unknown: true + visibility: + creator: public_only + field: any + getter: public_only + is-getter: public_only + setter: public_only + + datasource: + hikari: + minimum-idle: 10 + maximum-pool-size: 30 + auto-commit: true + idle-timeout: 30000 + pool-name: HikariCP + max-lifetime: 900000 + connection-timeout: 10000 + connection-test-query: SELECT 1 + validation-timeout: 1000 +logging: + level: + com.njzscloud.common.sichen.SysTaskMapper: off + com.njzscloud: debug + +mybatis-plus: + type-handlers-package: com.njzscloud.common.mp.support.handler.j + configuration: + default-enum-type-handler: com.njzscloud.common.mp.support.handler.e.EnumTypeHandlerDealer diff --git a/njzscloud-svr/src/main/resources/logback-spring.xml b/njzscloud-svr/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..1db3326 --- /dev/null +++ b/njzscloud-svr/src/main/resources/logback-spring.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + false + + ${console_pattern} + + + + + ${log_path}/${service_name}/${service_name}.log + + ${log_path}/${service_name}/%d{yyyy-MM, aux}/${service_name}.%d{yyyy-MM-dd}.%i.log.zip + 50MB + 30 + + + ${file_pattern} + + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysEndpointMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysEndpointMapper.xml new file mode 100644 index 0000000..b9267e7 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysEndpointMapper.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysMenuMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysMenuMapper.xml new file mode 100644 index 0000000..b4ff8bb --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysMenuMapper.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysResourceMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysResourceMapper.xml new file mode 100644 index 0000000..9686dd9 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysResourceMapper.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysRoleMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysRoleMapper.xml new file mode 100644 index 0000000..359df60 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysRoleMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysUserAccountMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysUserAccountMapper.xml new file mode 100644 index 0000000..2b43f9a --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysUserAccountMapper.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysUserMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysUserMapper.xml new file mode 100644 index 0000000..3d95cd2 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysUserMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/SysUserRoleMapper.xml b/njzscloud-svr/src/main/resources/mapper/SysUserRoleMapper.xml new file mode 100644 index 0000000..6cc5607 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/SysUserRoleMapper.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2c1906c --- /dev/null +++ b/pom.xml @@ -0,0 +1,201 @@ + + + 4.0.0 + + com.njzscloud + zsy-recycling-supervision + 0.0.1 + pom + + + njzscloud-common + njzscloud-svr + + + + 8 + 8 + UTF-8 + + 2.6.13 + + 3.5.12 + + 1.6.2 + 2.0.51 + 3.3.4 + 5.8.28 + 3.3.0 + 4.12.0 + 2.4.1 + 8.5.17 + + 3.3.1 + + + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + com.njzscloud + njzscloud-common-core + 0.0.1 + + + com.njzscloud + njzscloud-common-sichen + 0.0.1 + + + com.njzscloud + njzscloud-common-sn + 0.0.1 + + + com.njzscloud + njzscloud-common-mp + 0.0.1 + + + com.njzscloud + njzscloud-common-mvc + 0.0.1 + + + com.njzscloud + njzscloud-common-redis + 0.0.1 + + + com.njzscloud + njzscloud-common-minio + 0.0.1 + + + com.njzscloud + njzscloud-common-security + 0.0.1 + + + com.njzscloud + njzscloud-common-gen + 0.0.1 + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + io.minio + minio + ${minio.version} + + + cglib + cglib + ${cglib.version} + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + + org.projectlombok + lombok + 1.18.30 + + + cn.hutool + hutool-bom + ${hutool.version} + pom + import + + + com.baomidou + mybatis-plus-bom + ${mybatis-plus.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + ${project.name}-${project.version} + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + UTF-8 + + pdf + ico + eot + ttf + woff + woff2 + ftl + css + svg + js + otf + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + + diff --git a/xxl-job/pom.xml b/xxl-job/pom.xml new file mode 100644 index 0000000..228800e --- /dev/null +++ b/xxl-job/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + com.xuxueli + xxl-job + 2.4.1 + + pom + ${project.artifactId} + A distributed task scheduling framework. + https://www.xuxueli.com/ + + + 8 + 8 + UTF-8 + + + 2.6.13 + + 4.0.21 + 2.3.2 + + 3.3.1 + + + + xxl-job-admin + xxl-job-core + + + + + + + org.apache.groovy + groovy + ${apache.groovy.version} + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot.version} + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + ${project.name}-${project.version} + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + UTF-8 + + pdf + ico + eot + ttf + woff + woff2 + ftl + css + svg + js + otf + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + + diff --git a/xxl-job/xxl-job-admin/pom.xml b/xxl-job/xxl-job-admin/pom.xml new file mode 100644 index 0000000..be38e48 --- /dev/null +++ b/xxl-job/xxl-job-admin/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + com.xuxueli + xxl-job + 2.4.1 + + + xxl-job-admin + + jar + + + 8 + 8 + UTF-8 + + 2.4.1 + + + + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} + + + + + 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 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java new file mode 100644 index 0000000..fce10a8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java @@ -0,0 +1,16 @@ +package com.xxl.job.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-10-28 00:38:13 + */ +@SpringBootApplication +public class XxlJobAdminApplication { + + public static void main(String[] args) { + SpringApplication.run(XxlJobAdminApplication.class, args); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java new file mode 100644 index 0000000..eb63f0b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +public class IndexController { + + @Resource + private XxlJobService xxlJobService; + @Resource + private LoginService loginService; + + + @RequestMapping("/") + public String index(Model model) { + + Map dashboardMap = xxlJobService.dashboardInfo(); + model.addAllAttributes(dashboardMap); + + return "index"; + } + + @RequestMapping("/chartInfo") + @ResponseBody + public ReturnT> chartInfo(Date startDate, Date endDate) { + ReturnT> chartInfo = xxlJobService.chartInfo(startDate, endDate); + return chartInfo; + } + + @RequestMapping("/toLogin") + @PermissionLimit(limit=false) + public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) { + if (loginService.ifLogin(request, response) != null) { + modelAndView.setView(new RedirectView("/",true,false)); + return modelAndView; + } + return new ModelAndView("login"); + } + + @RequestMapping(value="login", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT loginDo(HttpServletRequest request, HttpServletResponse response, String userName, String password, String ifRemember){ + boolean ifRem = (ifRemember!=null && ifRemember.trim().length()>0 && "on".equals(ifRemember))?true:false; + return loginService.login(request, response, userName, password, ifRem); + } + + @RequestMapping(value="logout", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT logout(HttpServletRequest request, HttpServletResponse response){ + return loginService.logout(request, response); + } + + @RequestMapping("/help") + public String help() { + + /*if (!PermissionInterceptor.ifLogin(request)) { + return "redirect:/toLogin"; + }*/ + + return "help"; + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java new file mode 100644 index 0000000..aa51e73 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java @@ -0,0 +1,72 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.GsonTool; +import com.xxl.job.core.util.XxlJobRemotingUtil; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * Created by xuxueli on 17/5/10. + */ +@Controller +@RequestMapping("/api") +public class JobApiController { + + @Resource + private AdminBiz adminBiz; + + /** + * api + * + * @param uri + * @param data + * @return + */ + @RequestMapping("/{uri}") + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) { + + // valid + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support."); + } + if (uri==null || uri.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty."); + } + if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null + && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0 + && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) { + return new ReturnT(ReturnT.FAIL_CODE, "The access token is wrong."); + } + + // services mapping + if ("callback".equals(uri)) { + List callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class); + return adminBiz.callback(callbackParamList); + } else if ("registry".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registry(registryParam); + } else if ("registryRemove".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registryRemove(registryParam); + } else { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found."); + } + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java new file mode 100644 index 0000000..fe4a0e8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogGlueDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.glue.GlueTypeEnum; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.List; + +/** + * job code controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobcode") +public class JobCodeController { + + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, int jobId) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + List jobLogGlues = xxlJobLogGlueDao.findByJobId(jobId); + + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_gluetype_unvalid")); + } + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + + // Glue类型-字典 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); + + model.addAttribute("jobInfo", jobInfo); + model.addAttribute("jobLogGlues", jobLogGlues); + return "jobcode/jobcode.index"; + } + + @RequestMapping("/save") + @ResponseBody + public ReturnT save(Model model, int id, String glueSource, String glueRemark) { + // valid + if (glueRemark==null) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) ); + } + if (glueRemark.length()<4 || glueRemark.length()>100) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_remark_limit")); + } + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(id); + if (exists_jobInfo == null) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + + // update new code + exists_jobInfo.setGlueSource(glueSource); + exists_jobInfo.setGlueRemark(glueRemark); + exists_jobInfo.setGlueUpdatetime(new Date()); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + // log old code + XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue(); + xxlJobLogGlue.setJobId(exists_jobInfo.getId()); + xxlJobLogGlue.setGlueType(exists_jobInfo.getGlueType()); + xxlJobLogGlue.setGlueSource(glueSource); + xxlJobLogGlue.setGlueRemark(glueRemark); + + xxlJobLogGlue.setAddTime(new Date()); + xxlJobLogGlue.setUpdateTime(new Date()); + xxlJobLogGlueDao.save(xxlJobLogGlue); + + // remove code backup more than 30 + xxlJobLogGlueDao.removeOld(exists_jobInfo.getId(), 30); + + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java new file mode 100644 index 0000000..8e0c5a4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobRegistryDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * job group controller + * @author xuxueli 2016-10-02 20:52:56 + */ +@Controller +@RequestMapping("/jobgroup") +public class JobGroupController { + + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + return "jobgroup/jobgroup.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String appname, String title) { + + // page query + List list = xxlJobGroupDao.pageList(start, length, appname, title); + int list_count = xxlJobGroupDao.pageListCount(start, length, appname, title); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/save") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT save(XxlJobGroup xxlJobGroup){ + + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) { + return new ReturnT(500, "AppName"+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getAddressType()!=0) { + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") ); + } + + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.save(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT update(XxlJobGroup xxlJobGroup){ + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getAddressType() == 0) { + // 0=自动注册 + List registryList = findRegistryByAppName(xxlJobGroup.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + addressListStr = ""; + for (String item:registryList) { + addressListStr += item + ","; + } + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + xxlJobGroup.setAddressList(addressListStr); + } else { + // 1=手动录入 + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.update(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + private List findRegistryByAppName(String appnameParam){ + HashMap> appAddressMap = new HashMap>(); + List list = xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + return appAddressMap.get(appnameParam); + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT remove(int id){ + + // valid + int count = xxlJobInfoDao.pageListCount(0, 10, id, -1, null, null, null); + if (count > 0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_del_limit_0") ); + } + + List allList = xxlJobGroupDao.findAll(); + if (allList.size() == 1) { + return new ReturnT(500, I18nUtil.getString("jobgroup_del_limit_1") ); + } + + int ret = xxlJobGroupDao.remove(id); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/loadById") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT loadById(int id){ + XxlJobGroup jobGroup = xxlJobGroupDao.load(id); + return jobGroup!=null?new ReturnT(jobGroup):new ReturnT(ReturnT.FAIL_CODE, null); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java new file mode 100644 index 0000000..516dce4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java @@ -0,0 +1,172 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobinfo") +public class JobInfoController { + private static Logger logger = LoggerFactory.getLogger(JobInfoController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobService xxlJobService; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) { + + // 枚举-字典 + model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values()); // 路由策略-列表 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); // Glue类型-字典 + model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values()); // 阻塞处理策略-字典 + model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values()); // 调度类型 + model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values()); // 调度过期策略 + + // 执行器列表 + List jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List jobGroupList = filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + model.addAttribute("jobGroup", jobGroup); + + return "jobinfo/jobinfo.index"; + } + + public static List filterJobGroupByRole(HttpServletRequest request, List jobGroupList_all){ + List jobGroupList = new ArrayList<>(); + if (jobGroupList_all!=null && jobGroupList_all.size()>0) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getRole() == 1) { + jobGroupList = jobGroupList_all; + } else { + List groupIdStrs = new ArrayList<>(); + if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) { + groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(",")); + } + for (XxlJobGroup groupItem:jobGroupList_all) { + if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) { + jobGroupList.add(groupItem); + } + } + } + } + return jobGroupList; + } + public static void validPermission(HttpServletRequest request, int jobGroup) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (!loginUser.validPermission(jobGroup)) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]"); + } + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + } + + @RequestMapping("/add") + @ResponseBody + public ReturnT add(XxlJobInfo jobInfo) { + return xxlJobService.add(jobInfo); + } + + @RequestMapping("/update") + @ResponseBody + public ReturnT update(XxlJobInfo jobInfo) { + return xxlJobService.update(jobInfo); + } + + @RequestMapping("/remove") + @ResponseBody + public ReturnT remove(int id) { + return xxlJobService.remove(id); + } + + @RequestMapping("/stop") + @ResponseBody + public ReturnT pause(int id) { + return xxlJobService.stop(id); + } + + @RequestMapping("/start") + @ResponseBody + public ReturnT start(int id) { + return xxlJobService.start(id); + } + + @RequestMapping("/trigger") + @ResponseBody + public ReturnT triggerJob(HttpServletRequest request, int id, String executorParam, String addressList) { + // login user + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + // trigger + return xxlJobService.trigger(loginUser, id, executorParam, addressList); + } + + @RequestMapping("/nextTriggerTime") + @ResponseBody + public ReturnT> nextTriggerTime(String scheduleType, String scheduleConf) { + + XxlJobInfo paramXxlJobInfo = new XxlJobInfo(); + paramXxlJobInfo.setScheduleType(scheduleType); + paramXxlJobInfo.setScheduleConf(scheduleConf); + + List result = new ArrayList<>(); + try { + Date lastTime = new Date(); + for (int i = 0; i < 5; i++) { + lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime); + if (lastTime != null) { + result.add(DateUtil.formatDateTime(lastTime)); + } else { + break; + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage()); + } + return new ReturnT>(result); + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java new file mode 100644 index 0000000..bff9198 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java @@ -0,0 +1,246 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogDao; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.KillParam; +import com.xxl.job.core.biz.model.LogParam; +import com.xxl.job.core.biz.model.LogResult; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/joblog") +public class JobLogController { + private static Logger logger = LoggerFactory.getLogger(JobLogController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "0") Integer jobId) { + + // 执行器列表 + List jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List jobGroupList = JobInfoController.filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + + // 任务 + if (jobId > 0) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid")); + } + + model.addAttribute("jobInfo", jobInfo); + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + } + + return "joblog/joblog.index"; + } + + @RequestMapping("/getJobsByGroup") + @ResponseBody + public ReturnT> getJobsByGroup(int jobGroup){ + List list = xxlJobInfoDao.getJobsByGroup(jobGroup); + return new ReturnT>(list); + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int jobId, int logStatus, String filterTime) { + + // valid permission + JobInfoController.validPermission(request, jobGroup); // 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup + + // parse param + Date triggerTimeStart = null; + Date triggerTimeEnd = null; + if (filterTime!=null && filterTime.trim().length()>0) { + String[] temp = filterTime.split(" - "); + if (temp.length == 2) { + triggerTimeStart = DateUtil.parseDateTime(temp[0]); + triggerTimeEnd = DateUtil.parseDateTime(temp[1]); + } + } + + // page query + List list = xxlJobLogDao.pageList(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + int list_count = xxlJobLogDao.pageListCount(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/logDetailPage") + public String logDetailPage(int id, Model model){ + + // base check + ReturnT logStatue = ReturnT.SUCCESS; + XxlJobLog jobLog = xxlJobLogDao.load(id); + if (jobLog == null) { + throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid")); + } + + model.addAttribute("triggerCode", jobLog.getTriggerCode()); + model.addAttribute("handleCode", jobLog.getHandleCode()); + model.addAttribute("logId", jobLog.getId()); + return "joblog/joblog.detail"; + } + + @RequestMapping("/logDetailCat") + @ResponseBody + public ReturnT logDetailCat(long logId, int fromLineNum){ + try { + // valid + XxlJobLog jobLog = xxlJobLogDao.load(logId); // todo, need to improve performance + if (jobLog == null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_logid_unvalid")); + } + + // log cat + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(jobLog.getExecutorAddress()); + ReturnT logResult = executorBiz.log(new LogParam(jobLog.getTriggerTime().getTime(), logId, fromLineNum)); + + // is end + if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) { + if (jobLog.getHandleCode() > 0) { + logResult.getContent().setEnd(true); + } + } + + // fix xss + if (logResult.getContent()!=null && StringUtils.hasText(logResult.getContent().getLogContent())) { + String newLogContent = logResult.getContent().getLogContent(); + newLogContent = HtmlUtils.htmlEscape(newLogContent, "UTF-8"); + logResult.getContent().setLogContent(newLogContent); + } + + return logResult; + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, e.getMessage()); + } + } + + @RequestMapping("/logKill") + @ResponseBody + public ReturnT logKill(int id){ + // base check + XxlJobLog log = xxlJobLogDao.load(id); + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId()); + if (jobInfo==null) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) { + return new ReturnT(500, I18nUtil.getString("joblog_kill_log_limit")); + } + + // request of kill + ReturnT runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress()); + runResult = executorBiz.kill(new KillParam(jobInfo.getId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + runResult = new ReturnT(500, e.getMessage()); + } + + if (ReturnT.SUCCESS_CODE == runResult.getCode()) { + log.setHandleCode(ReturnT.FAIL_CODE); + log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():"")); + log.setHandleTime(new Date()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + return new ReturnT(runResult.getMsg()); + } else { + return new ReturnT(500, runResult.getMsg()); + } + } + + @RequestMapping("/clearLog") + @ResponseBody + public ReturnT clearLog(int jobGroup, int jobId, int type){ + + Date clearBeforeTime = null; + int clearBeforeNum = 0; + if (type == 1) { + clearBeforeTime = DateUtil.addMonths(new Date(), -1); // 清理一个月之前日志数据 + } else if (type == 2) { + clearBeforeTime = DateUtil.addMonths(new Date(), -3); // 清理三个月之前日志数据 + } else if (type == 3) { + clearBeforeTime = DateUtil.addMonths(new Date(), -6); // 清理六个月之前日志数据 + } else if (type == 4) { + clearBeforeTime = DateUtil.addYears(new Date(), -1); // 清理一年之前日志数据 + } else if (type == 5) { + clearBeforeNum = 1000; // 清理一千条以前日志数据 + } else if (type == 6) { + clearBeforeNum = 10000; // 清理一万条以前日志数据 + } else if (type == 7) { + clearBeforeNum = 30000; // 清理三万条以前日志数据 + } else if (type == 8) { + clearBeforeNum = 100000; // 清理十万条以前日志数据 + } else if (type == 9) { + clearBeforeNum = 0; // 清理所有日志数据 + } else { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_clean_type_unvalid")); + } + + List logIds = null; + do { + logIds = xxlJobLogDao.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000); + if (logIds!=null && logIds.size()>0) { + xxlJobLogDao.clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java new file mode 100644 index 0000000..3f4c755 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java @@ -0,0 +1,179 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author xuxueli 2019-05-04 16:39:50 + */ +@Controller +@RequestMapping("/user") +public class UserController { + + @Resource + private XxlJobUserDao xxlJobUserDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + + // 执行器列表 + List groupList = xxlJobGroupDao.findAll(); + model.addAttribute("groupList", groupList); + + return "user/user.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String username, int role) { + + // page list + List list = xxlJobUserDao.pageList(start, length, username, role); + int list_count = xxlJobUserDao.pageListCount(start, length, username, role); + + // filter + if (list!=null && list.size()>0) { + for (XxlJobUser item: list) { + item.setPassword(null); + } + } + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/add") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT add(XxlJobUser xxlJobUser) { + + // valid username + if (!StringUtils.hasText(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") ); + } + xxlJobUser.setUsername(xxlJobUser.getUsername().trim()); + if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // valid password + if (!StringUtils.hasText(xxlJobUser.getPassword())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") ); + } + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + + // check repeat + XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername()); + if (existUser != null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") ); + } + + // write + xxlJobUserDao.save(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT update(HttpServletRequest request, XxlJobUser xxlJobUser) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getUsername().equals(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + // valid password + if (StringUtils.hasText(xxlJobUser.getPassword())) { + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + } else { + xxlJobUser.setPassword(null); + } + + // write + xxlJobUserDao.update(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT remove(HttpServletRequest request, int id) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getId() == id) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + xxlJobUserDao.delete(id); + return ReturnT.SUCCESS; + } + + @RequestMapping("/updatePwd") + @ResponseBody + public ReturnT updatePwd(HttpServletRequest request, String password){ + + // valid password + if (password==null || password.trim().length()==0){ + return new ReturnT(ReturnT.FAIL.getCode(), "密码不可为空"); + } + password = password.trim(); + if (!(password.length()>=4 && password.length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + + // md5 password + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + + // update pwd + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + + // do write + XxlJobUser existUser = xxlJobUserDao.loadByUserName(loginUser.getUsername()); + existUser.setPassword(md5Password); + xxlJobUserDao.update(existUser); + + return ReturnT.SUCCESS; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java new file mode 100644 index 0000000..379efd4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java @@ -0,0 +1,29 @@ +package com.xxl.job.admin.controller.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限限制 + * @author xuxueli 2015-12-12 18:29:02 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PermissionLimit { + + /** + * 登录拦截 (默认拦截) + */ + boolean limit() default true; + + /** + * 要求管理员权限 + * + * @return + */ + boolean adminuser() default false; + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java new file mode 100644 index 0000000..592d496 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java @@ -0,0 +1,42 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.core.util.FtlUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; + +/** + * push cookies to model as cookieMap + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class CookieInterceptor implements AsyncHandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + + // cookie + if (modelAndView!=null && request.getCookies()!=null && request.getCookies().length>0) { + HashMap cookieMap = new HashMap(); + for (Cookie ck : request.getCookies()) { + cookieMap.put(ck.getName(), ck); + } + modelAndView.addObject("cookieMap", cookieMap); + } + + // static method + if (modelAndView != null) { + modelAndView.addObject("I18nUtil", FtlUtil.generateStaticModel(I18nUtil.class.getName())); + } + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java new file mode 100644 index 0000000..840f0eb --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java @@ -0,0 +1,59 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.service.LoginService; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 权限拦截 + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class PermissionInterceptor implements AsyncHandlerInterceptor { + + @Resource + private LoginService loginService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (!(handler instanceof HandlerMethod)) { + return true; // proceed with the next interceptor + } + + // if need login + boolean needLogin = true; + boolean needAdminuser = false; + HandlerMethod method = (HandlerMethod)handler; + PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class); + if (permission!=null) { + needLogin = permission.limit(); + needAdminuser = permission.adminuser(); + } + + if (needLogin) { + XxlJobUser loginUser = loginService.ifLogin(request, response); + if (loginUser == null) { + response.setStatus(302); + response.setHeader("location", request.getContextPath()+"/toLogin"); + return false; + } + if (needAdminuser && loginUser.getRole()!=1) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit")); + } + request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser); + } + + return true; // proceed with the next interceptor + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java new file mode 100644 index 0000000..0be6ba6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java @@ -0,0 +1,28 @@ +package com.xxl.job.admin.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; + +/** + * web mvc config + * + * @author xuxueli 2018-04-02 20:48:20 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Resource + private PermissionInterceptor permissionInterceptor; + @Resource + private CookieInterceptor cookieInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(permissionInterceptor).addPathPatterns("/**"); + registry.addInterceptor(cookieInterceptor).addPathPatterns("/**"); + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java new file mode 100644 index 0000000..114407b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java @@ -0,0 +1,66 @@ +package com.xxl.job.admin.controller.resolver; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.admin.core.util.JacksonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * common exception resolver + * + * @author xuxueli 2016-1-6 19:22:18 + */ +@Component +public class WebExceptionResolver implements HandlerExceptionResolver { + private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class); + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + + if (!(ex instanceof XxlJobException)) { + logger.error("WebExceptionResolver:{}", ex); + } + + // if json + boolean isJson = false; + if (handler instanceof HandlerMethod) { + HandlerMethod method = (HandlerMethod)handler; + ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class); + if (responseBody != null) { + isJson = true; + } + } + + // error result + ReturnT errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "
")); + + // response + ModelAndView mv = new ModelAndView(); + if (isJson) { + try { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JacksonUtil.writeValueAsString(errorResult)); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return mv; + } else { + + mv.addObject("exceptionMsg", errorResult.getMsg()); + mv.setViewName("/common/common.exception"); + return mv; + } + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java new file mode 100644 index 0000000..4165ff3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java @@ -0,0 +1,20 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; + +/** + * @author xuxueli 2020-01-19 + */ +public interface JobAlarm { + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java new file mode 100644 index 0000000..797dc90 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java @@ -0,0 +1,65 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class JobAlarmer implements ApplicationContextAware, InitializingBean { + private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class); + + private ApplicationContext applicationContext; + private List 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 (serviceBeanMap != null && serviceBeanMap.size() > 0) { + jobAlarmList = new ArrayList(serviceBeanMap.values()); + } + } + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) { + + boolean result = false; + if (jobAlarmList!=null && jobAlarmList.size()>0) { + result = true; // success means all-success + for (JobAlarm alarm: jobAlarmList) { + boolean resultItem = false; + try { + resultItem = alarm.doAlarm(info, jobLog); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + if (!resultItem) { + result = false; + } + } + } + + return result; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java new file mode 100644 index 0000000..16e5218 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java @@ -0,0 +1,118 @@ +package com.xxl.job.admin.core.alarm.impl; + +import com.xxl.job.admin.core.alarm.JobAlarm; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.internet.MimeMessage; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * job alarm by email + * + * @author xuxueli 2020-01-19 + */ +@Component +public class EmailJobAlarm implements JobAlarm { + private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class); + + /** + * fail alarm + * + * @param jobLog + */ + @Override + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){ + boolean alarmResult = true; + + // send monitor email + if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) { + + // alarmContent + String alarmContent = "Alarm Job LogId=" + jobLog.getId(); + if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "
TriggerMsg=
" + jobLog.getTriggerMsg(); + } + if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "
HandleCode=" + jobLog.getHandleMsg(); + } + + // email info + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(Integer.valueOf(info.getJobGroup())); + String personal = I18nUtil.getString("admin_name_full"); + String title = I18nUtil.getString("jobconf_monitor"); + String content = MessageFormat.format(loadEmailJobAlarmTemplate(), + group!=null?group.getTitle():"null", + info.getId(), + info.getJobDesc(), + alarmContent); + + Set emailSet = new HashSet(Arrays.asList(info.getAlarmEmail().split(","))); + for (String email: emailSet) { + + // make mail + try { + MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal); + helper.setTo(email); + helper.setSubject(title); + helper.setText(content, true); + + XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e); + + alarmResult = false; + } + + } + } + + return alarmResult; + } + + /** + * load email job alarm template + * + * @return + */ + private static final String loadEmailJobAlarmTemplate(){ + String mailBodyTemplate = "
" + 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; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java new file mode 100644 index 0000000..279ad7d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java @@ -0,0 +1,99 @@ +package com.xxl.job.admin.core.complete; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.MessageFormat; + +/** + * @author xuxueli 2020-10-30 20:43:10 + */ +public class XxlJobCompleter { + private static Logger logger = LoggerFactory.getLogger(XxlJobCompleter.class); + + /** + * common fresh handle entrance (limit only once) + * + * @param xxlJobLog + * @return + */ + public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) { + + // finish + finishJob(xxlJobLog); + + // text最大64kb 避免长度过长 + if (xxlJobLog.getHandleMsg().length() > 15000) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) ); + } + + // fresh handle + return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog); + } + + + /** + * do somethind to finish job + */ + private static void finishJob(XxlJobLog xxlJobLog){ + + // 1、handle success, to trigger child job + String triggerChildMsg = null; + if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) { + XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId()); + if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) { + triggerChildMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<<
"; + + String[] childJobIds = xxlJobInfo.getChildJobId().split(","); + for (int i = 0; i < childJobIds.length; i++) { + int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1; + if (childJobId > 0) { + + JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null); + ReturnT triggerChildResult = ReturnT.SUCCESS; + + // add msg + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"), + (i+1), + childJobIds.length, + childJobIds[i], + (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")), + triggerChildResult.getMsg()); + } else { + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"), + (i+1), + childJobIds.length, + childJobIds[i]); + } + } + + } + } + + if (triggerChildMsg != null) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg ); + } + + // 2、fix_delay trigger next + // on the way + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java new file mode 100644 index 0000000..380b8a5 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java @@ -0,0 +1,158 @@ +package com.xxl.job.admin.core.conf; + +import com.xxl.job.admin.core.alarm.JobAlarmer; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.dao.*; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.util.Arrays; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ + +@Component +public class XxlJobAdminConfig implements InitializingBean, DisposableBean { + + private static XxlJobAdminConfig adminConfig = null; + public static XxlJobAdminConfig getAdminConfig() { + return adminConfig; + } + + + // ---------------------- XxlJobScheduler ---------------------- + + private XxlJobScheduler xxlJobScheduler; + + @Override + public void afterPropertiesSet() throws Exception { + adminConfig = this; + + xxlJobScheduler = new XxlJobScheduler(); + xxlJobScheduler.init(); + } + + @Override + public void destroy() throws Exception { + xxlJobScheduler.destroy(); + } + + + // ---------------------- XxlJobScheduler ---------------------- + + // conf + @Value("${xxl.job.i18n}") + private String i18n; + + @Value("${xxl.job.accessToken}") + private String accessToken; + + @Value("${spring.mail.from}") + private String emailFrom; + + @Value("${xxl.job.triggerpool.fast.max}") + private int triggerPoolFastMax; + + @Value("${xxl.job.triggerpool.slow.max}") + private int triggerPoolSlowMax; + + @Value("${xxl.job.logretentiondays}") + private int logretentiondays; + + // dao, service + + @Resource + private XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + @Resource + private JavaMailSender mailSender; + @Resource + private DataSource dataSource; + @Resource + private JobAlarmer jobAlarmer; + + + public String getI18n() { + if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) { + return "zh_CN"; + } + return i18n; + } + + public String getAccessToken() { + return accessToken; + } + + public String getEmailFrom() { + return emailFrom; + } + + public int getTriggerPoolFastMax() { + if (triggerPoolFastMax < 200) { + return 200; + } + return triggerPoolFastMax; + } + + public int getTriggerPoolSlowMax() { + if (triggerPoolSlowMax < 100) { + return 100; + } + return triggerPoolSlowMax; + } + + public int getLogretentiondays() { + if (logretentiondays < 7) { + return -1; // Limit greater than or equal to 7, otherwise close + } + return logretentiondays; + } + + public XxlJobLogDao getXxlJobLogDao() { + return xxlJobLogDao; + } + + public XxlJobInfoDao getXxlJobInfoDao() { + return xxlJobInfoDao; + } + + public XxlJobRegistryDao getXxlJobRegistryDao() { + return xxlJobRegistryDao; + } + + public XxlJobGroupDao getXxlJobGroupDao() { + return xxlJobGroupDao; + } + + public XxlJobLogReportDao getXxlJobLogReportDao() { + return xxlJobLogReportDao; + } + + public JavaMailSender getMailSender() { + return mailSender; + } + + public DataSource getDataSource() { + return dataSource; + } + + public JobAlarmer getJobAlarmer() { + return jobAlarmer; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java new file mode 100644 index 0000000..1364b44 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java @@ -0,0 +1,1666 @@ +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package com.xxl.job.admin.core.cron; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; + +/** + * Provides a parser and evaluator for unix-like cron expressions. Cron + * expressions provide the ability to specify complex time combinations such as + * "At 8:00am every Monday through Friday" or "At 1:30am every + * last Friday of the month". + *

+ * Cron expressions are comprised of 6 required fields and one optional field + * separated by white space. The fields respectively are described as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
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.3.1 + * + */ +public final class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map 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 months; + protected transient TreeSet daysOfWeek; + protected transient TreeSet years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws 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(); + } + 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 (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); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet 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.toString() + ")", 0); + } + } + + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if(type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if(c == '-') { + ValueSet vs = getValue(0, s, i+1); + lastdayOffset = vs.value; + if(lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i+1); + i = vs.pos; + } + if(s.length() > i) { + c = s.charAt(i); + if(c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if(val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if(val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.Set 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.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if(!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if(t > day) { + mon++; + if(mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if(nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(Calendar.HOUR_OF_DAY, hour); + if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(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 void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java new file mode 100644 index 0000000..faa6063 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java @@ -0,0 +1,14 @@ +package com.xxl.job.admin.core.exception; + +/** + * @author xuxueli 2019-05-04 23:19:29 + */ +public class XxlJobException extends RuntimeException { + + public XxlJobException() { + } + public XxlJobException(String message) { + super(message); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java new file mode 100644 index 0000000..dde4b39 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java @@ -0,0 +1,77 @@ +package com.xxl.job.admin.core.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobGroup { + + private int id; + private String appname; + private String title; + private int addressType; // 执行器地址类型:0=自动注册、1=手动录入 + private String addressList; // 执行器地址列表,多地址逗号分隔(手动录入) + private Date updateTime; + + // registry list + private List registryList; // 执行器地址列表(系统注册) + public List getRegistryList() { + if (addressList!=null && addressList.trim().length()>0) { + 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; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java new file mode 100644 index 0000000..e47b6dc --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java @@ -0,0 +1,237 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job info + * + * @author xuxueli 2016-1-12 18:25:49 + */ +public class XxlJobInfo { + + private int id; // 主键ID + + private int jobGroup; // 执行器主键ID + private String jobDesc; + + private Date addTime; + private Date updateTime; + + private String author; // 负责人 + private String alarmEmail; // 报警邮件 + + private String scheduleType; // 调度类型 + private String scheduleConf; // 调度配置,值含义取决于调度类型 + private String misfireStrategy; // 调度过期策略 + + private String executorRouteStrategy; // 执行器路由策略 + private String executorHandler; // 执行器,任务Handler名称 + private String executorParam; // 执行器,任务参数 + private String executorBlockStrategy; // 阻塞处理策略 + private int executorTimeout; // 任务执行超时时间,单位秒 + private int executorFailRetryCount; // 失败重试次数 + + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; // GLUE源代码 + private String glueRemark; // GLUE备注 + private Date glueUpdatetime; // GLUE更新时间 + + private String childJobId; // 子任务ID,多个逗号分隔 + + private int triggerStatus; // 调度状态:0-停止,1-运行 + private long triggerLastTime; // 上次调度时间 + private long triggerNextTime; // 下次调度时间 + + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public String getJobDesc() { + return jobDesc; + } + + public void setJobDesc(String jobDesc) { + this.jobDesc = jobDesc; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getAlarmEmail() { + return alarmEmail; + } + + public void setAlarmEmail(String alarmEmail) { + this.alarmEmail = alarmEmail; + } + + public String getScheduleType() { + return scheduleType; + } + + public void setScheduleType(String scheduleType) { + this.scheduleType = scheduleType; + } + + public String getScheduleConf() { + return scheduleConf; + } + + public void setScheduleConf(String scheduleConf) { + this.scheduleConf = scheduleConf; + } + + public String getMisfireStrategy() { + return misfireStrategy; + } + + public void setMisfireStrategy(String misfireStrategy) { + this.misfireStrategy = misfireStrategy; + } + + public String getExecutorRouteStrategy() { + return executorRouteStrategy; + } + + public void setExecutorRouteStrategy(String executorRouteStrategy) { + this.executorRouteStrategy = executorRouteStrategy; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorBlockStrategy() { + return executorBlockStrategy; + } + + public void setExecutorBlockStrategy(String executorBlockStrategy) { + this.executorBlockStrategy = executorBlockStrategy; + } + + public int getExecutorTimeout() { + return executorTimeout; + } + + public void setExecutorTimeout(int executorTimeout) { + this.executorTimeout = executorTimeout; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getGlueUpdatetime() { + return glueUpdatetime; + } + + public void setGlueUpdatetime(Date glueUpdatetime) { + this.glueUpdatetime = glueUpdatetime; + } + + public String getChildJobId() { + return childJobId; + } + + public void setChildJobId(String childJobId) { + this.childJobId = childJobId; + } + + public int getTriggerStatus() { + return triggerStatus; + } + + public void setTriggerStatus(int triggerStatus) { + this.triggerStatus = triggerStatus; + } + + public long getTriggerLastTime() { + return triggerLastTime; + } + + public void setTriggerLastTime(long triggerLastTime) { + this.triggerLastTime = triggerLastTime; + } + + public long getTriggerNextTime() { + return triggerNextTime; + } + + public void setTriggerNextTime(long triggerNextTime) { + this.triggerNextTime = triggerNextTime; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java new file mode 100644 index 0000000..7d3072a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java @@ -0,0 +1,157 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log, used to track trigger process + * @author xuxueli 2015-12-19 23:19:09 + */ +public class XxlJobLog { + + private long id; + + // job info + private int jobGroup; + private int jobId; + + // execute info + private String executorAddress; + private String executorHandler; + private String executorParam; + private String executorShardingParam; + private int executorFailRetryCount; + + // trigger info + private Date triggerTime; + private int triggerCode; + private String triggerMsg; + + // handle info + private Date handleTime; + private int handleCode; + private String handleMsg; + + // alarm info + private int alarmStatus; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getExecutorAddress() { + return executorAddress; + } + + public void setExecutorAddress(String executorAddress) { + this.executorAddress = executorAddress; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorShardingParam() { + return executorShardingParam; + } + + public void setExecutorShardingParam(String executorShardingParam) { + this.executorShardingParam = executorShardingParam; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public Date getTriggerTime() { + return triggerTime; + } + + public void setTriggerTime(Date triggerTime) { + this.triggerTime = triggerTime; + } + + public int getTriggerCode() { + return triggerCode; + } + + public void setTriggerCode(int triggerCode) { + this.triggerCode = triggerCode; + } + + public String getTriggerMsg() { + return triggerMsg; + } + + public void setTriggerMsg(String triggerMsg) { + this.triggerMsg = triggerMsg; + } + + public Date getHandleTime() { + return handleTime; + } + + public void setHandleTime(Date handleTime) { + this.handleTime = handleTime; + } + + public int getHandleCode() { + return handleCode; + } + + public void setHandleCode(int handleCode) { + this.handleCode = handleCode; + } + + public String getHandleMsg() { + return handleMsg; + } + + public void setHandleMsg(String handleMsg) { + this.handleMsg = handleMsg; + } + + public int getAlarmStatus() { + return alarmStatus; + } + + public void setAlarmStatus(int alarmStatus) { + this.alarmStatus = alarmStatus; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java new file mode 100644 index 0000000..2f59ffa --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java @@ -0,0 +1,75 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log for glue, used to track job code process + * @author xuxueli 2016-5-19 17:57:46 + */ +public class XxlJobLogGlue { + + private int id; + private int jobId; // 任务主键ID + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; + private String glueRemark; + private Date addTime; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java new file mode 100644 index 0000000..e58ff1a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java @@ -0,0 +1,54 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +public class XxlJobLogReport { + + private int id; + + private Date triggerDay; + + private int runningCount; + private int sucCount; + private int failCount; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Date getTriggerDay() { + return triggerDay; + } + + public void setTriggerDay(Date triggerDay) { + this.triggerDay = triggerDay; + } + + public int getRunningCount() { + return runningCount; + } + + public void setRunningCount(int runningCount) { + this.runningCount = runningCount; + } + + public int getSucCount() { + return sucCount; + } + + public void setSucCount(int sucCount) { + this.sucCount = sucCount; + } + + public int getFailCount() { + return failCount; + } + + public void setFailCount(int failCount) { + this.failCount = failCount; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java new file mode 100644 index 0000000..924d6d3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java @@ -0,0 +1,55 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobRegistry { + + private int id; + private String registryGroup; + private String registryKey; + private String registryValue; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getRegistryGroup() { + return registryGroup; + } + + public void setRegistryGroup(String registryGroup) { + this.registryGroup = registryGroup; + } + + public String getRegistryKey() { + return registryKey; + } + + public void setRegistryKey(String registryKey) { + this.registryKey = registryKey; + } + + public String getRegistryValue() { + return registryValue; + } + + public void setRegistryValue(String registryValue) { + this.registryValue = registryValue; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java new file mode 100644 index 0000000..db17327 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java @@ -0,0 +1,73 @@ +package com.xxl.job.admin.core.model; + +import org.springframework.util.StringUtils; + +/** + * @author xuxueli 2019-05-04 16:43:12 + */ +public class XxlJobUser { + + private int id; + private String username; // 账号 + private String password; // 密码 + private int role; // 角色:0-普通用户、1-管理员 + private String permission; // 权限:执行器ID列表,多个逗号分割 + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRole() { + return role; + } + + public void setRole(int role) { + this.role = role; + } + + public String getPermission() { + return permission; + } + + public void setPermission(String permission) { + this.permission = permission; + } + + // plugin + public boolean validPermission(int jobGroup){ + if (this.role == 1) { + return true; + } else { + if (StringUtils.hasText(this.permission)) { + for (String permissionItem : this.permission.split(",")) { + if (String.valueOf(jobGroup).equals(permissionItem)) { + return true; + } + } + } + return false; + } + + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java new file mode 100644 index 0000000..5d1f2a0 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java @@ -0,0 +1,32 @@ +package com.xxl.job.admin.core.old;//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()); +// +// +// } +// +//} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java new file mode 100644 index 0000000..5ae81bd --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java @@ -0,0 +1,413 @@ +package com.xxl.job.admin.core.old;//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; +// }*/ +// +//} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java new file mode 100644 index 0000000..74f3f9d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java @@ -0,0 +1,58 @@ +package com.xxl.job.admin.core.old;//package com.xxl.job.admin.core.quartz; +// +//import org.quartz.SchedulerConfigException; +//import org.quartz.spi.ThreadPool; +// +///** +// * single thread pool, for async trigger +// * +// * @author xuxueli 2019-03-06 +// */ +//public class XxlJobThreadPool implements ThreadPool { +// +// @Override +// public boolean runInThread(Runnable runnable) { +// +// // async run +// runnable.run(); +// return true; +// +// //return false; +// } +// +// @Override +// public int blockForAvailableThreads() { +// return 1; +// } +// +// @Override +// public void initialize() throws SchedulerConfigException { +// +// } +// +// @Override +// public void shutdown(boolean waitForJobsToComplete) { +// +// } +// +// @Override +// public int getPoolSize() { +// return 1; +// } +// +// @Override +// public void setInstanceId(String schedInstId) { +// +// } +// +// @Override +// public void setInstanceName(String schedName) { +// +// } +// +// // support +// public void setThreadCount(int count) { +// // +// } +// +//} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java new file mode 100644 index 0000000..7fff93a --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.admin.core.route.strategy.*; +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * Created by xuxueli on 17/3/10. + */ +public enum ExecutorRouteStrategyEnum { + + FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()), + LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()), + ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()), + RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()), + CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()), + LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()), + LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()), + FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()), + BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()), + SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null); + + ExecutorRouteStrategyEnum(String title, ExecutorRouter router) { + this.title = title; + this.router = router; + } + + private String title; + private ExecutorRouter router; + + public String getTitle() { + return title; + } + public ExecutorRouter getRouter() { + return router; + } + + public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){ + if (name != null) { + for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + } + return defaultItem; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java new file mode 100644 index 0000000..5de9a1d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public abstract class ExecutorRouter { + protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class); + + /** + * route address + * + * @param addressList + * @return ReturnT.content=address + */ + public abstract ReturnT route(TriggerParam triggerParam, List addressList); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java new file mode 100644 index 0000000..868560f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.IdleBeatParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteBusyover extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + StringBuffer idleBeatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT idleBeatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + idleBeatResult = new ReturnT(ReturnT.FAIL_CODE, ""+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.getCode() == ReturnT.SUCCESS_CODE) { + idleBeatResult.setMsg(idleBeatResultSB.toString()); + idleBeatResult.setContent(address); + return idleBeatResult; + } + } + + return new ReturnT(ReturnT.FAIL_CODE, idleBeatResultSB.toString()); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java new file mode 100644 index 0000000..41ac671 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java @@ -0,0 +1,85 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器; + * a、virtual node:解决不均衡问题 + * b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围 + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteConsistentHash extends ExecutorRouter { + + private static int VIRTUAL_NODE_NUM = 100; + + /** + * get hash code on 2^32 ring (md5散列的方式计算hash值) + * @param key + * @return + */ + private static long hash(String key) { + + // md5 byte + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + md5.reset(); + byte[] keyBytes = null; + try { + keyBytes = key.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unknown string :" + key, e); + } + + md5.update(keyBytes); + byte[] digest = md5.digest(); + + // hash code, Truncate to 32-bits + long hashCode = ((long) (digest[3] & 0xFF) << 24) + | ((long) (digest[2] & 0xFF) << 16) + | ((long) (digest[1] & 0xFF) << 8) + | (digest[0] & 0xFF); + + long truncateHashCode = hashCode & 0xffffffffL; + return truncateHashCode; + } + + public String hashJob(int jobId, List 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 ReturnT route(TriggerParam triggerParam, List addressList) { + String address = hashJob(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java new file mode 100644 index 0000000..a2e4c90 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFailover extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + + StringBuffer beatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT beatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + beatResult = executorBiz.beat(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + beatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e ); + } + 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.getCode() == ReturnT.SUCCESS_CODE) { + + beatResult.setMsg(beatResultSB.toString()); + beatResult.setContent(address); + return beatResult; + } + } + return new ReturnT(ReturnT.FAIL_CODE, beatResultSB.toString()); + + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java new file mode 100644 index 0000000..de4d7af --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFirst extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList){ + return new ReturnT(addressList.get(0)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java new file mode 100644 index 0000000..9df1972 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,使用频率最低的优先被选举 + * a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLFU extends ExecutorRouter { + + private static ConcurrentMap> 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.size() > 0) { + for (String delKey: delKeys) { + lfuItemMap.remove(delKey); + } + } + + // load least userd count address + List> lfuItemList = new ArrayList>(lfuItemMap.entrySet()); + Collections.sort(lfuItemList, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return o1.getValue().compareTo(o2.getValue()); + } + }); + + Map.Entry addressItem = lfuItemList.get(0); + String minAddress = addressItem.getKey(); + addressItem.setValue(addressItem.getValue() + 1); + + return addressItem.getKey(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java new file mode 100644 index 0000000..2d54006 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java @@ -0,0 +1,76 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,最久为使用的优先被选举 + * a、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b(*)、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLRU extends ExecutorRouter { + + private static ConcurrentMap> 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.size() > 0) { + for (String delKey: delKeys) { + lruItem.remove(delKey); + } + } + + // load + String eldestKey = lruItem.entrySet().iterator().next().getKey(); + String eldestValue = lruItem.get(eldestKey); + return eldestValue; + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java new file mode 100644 index 0000000..4ff3cf6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLast extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + return new ReturnT(addressList.get(addressList.size()-1)); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java new file mode 100644 index 0000000..5ea4a38 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java @@ -0,0 +1,23 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRandom extends ExecutorRouter { + + private static Random localRandom = new Random(); + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = addressList.get(localRandom.nextInt(addressList.size())); + return new ReturnT(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java new file mode 100644 index 0000000..d0ea2ba --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRound extends ExecutorRouter { + + private static ConcurrentMap routeCountEachJob = new ConcurrentHashMap<>(); + private static long CACHE_VALID_TIME = 0; + + private static int count(int jobId) { + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + routeCountEachJob.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + AtomicInteger count = routeCountEachJob.get(jobId); + if (count == null || count.get() > 1000000) { + // 初始化时主动Random一次,缓解首次压力 + count = new AtomicInteger(new Random().nextInt(100)); + } else { + // count++ + count.addAndGet(1); + } + routeCountEachJob.put(jobId, count); + return count.get(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = addressList.get(count(triggerParam.getJobId())%addressList.size()); + return new ReturnT(address); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java new file mode 100644 index 0000000..0b9b4a9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java @@ -0,0 +1,39 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum MisfireStrategyEnum { + + /** + * do nothing + */ + DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing")), + + /** + * fire once now + */ + FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now")); + + private String title; + + MisfireStrategyEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){ + for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java new file mode 100644 index 0000000..aa334fd --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum ScheduleTypeEnum { + + NONE(I18nUtil.getString("schedule_type_none")), + + /** + * schedule by cron + */ + CRON(I18nUtil.getString("schedule_type_cron")), + + /** + * schedule by fixed rate (in seconds) + */ + FIX_RATE(I18nUtil.getString("schedule_type_fix_rate")), + + /** + * schedule by fix delay (in seconds), after the last time + */ + /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/; + + private String title; + + ScheduleTypeEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){ + for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java new file mode 100644 index 0000000..bb2cda8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java @@ -0,0 +1,101 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.thread.*; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.client.ExecutorBizClient; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author xuxueli 2018-10-28 00:18:17 + */ + +public class XxlJobScheduler { + private static final Logger logger = LoggerFactory.getLogger(XxlJobScheduler.class); + + + public void init() throws Exception { + // init i18n + initI18n(); + + // admin trigger pool start + JobTriggerPoolHelper.toStart(); + + // admin registry monitor run + JobRegistryHelper.getInstance().start(); + + // admin fail-monitor run + JobFailMonitorHelper.getInstance().start(); + + // admin lose-monitor run ( depend on JobTriggerPoolHelper ) + JobCompleteHelper.getInstance().start(); + + // admin log report start + JobLogReportHelper.getInstance().start(); + + // start-schedule ( depend on JobTriggerPoolHelper ) + JobScheduleHelper.getInstance().start(); + + logger.info(">>>>>>>>> init xxl-job admin success."); + } + + + public void destroy() throws Exception { + + // stop-schedule + JobScheduleHelper.getInstance().toStop(); + + // admin log report stop + JobLogReportHelper.getInstance().toStop(); + + // admin lose-monitor stop + JobCompleteHelper.getInstance().toStop(); + + // admin fail-monitor stop + JobFailMonitorHelper.getInstance().toStop(); + + // admin registry stop + JobRegistryHelper.getInstance().toStop(); + + // admin trigger pool stop + JobTriggerPoolHelper.toStop(); + + } + + // ---------------------- I18n ---------------------- + + private void initI18n(){ + for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { + item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); + } + } + + // ---------------------- executor-client ---------------------- + private static ConcurrentMap 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 = new ExecutorBizClient(address, XxlJobAdminConfig.getAdminConfig().getAccessToken()); + + executorBizRepository.put(address, executorBiz); + return executorBiz; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java new file mode 100644 index 0000000..5698926 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java @@ -0,0 +1,184 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; + +/** + * job lose-monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobCompleteHelper { + private static Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class); + + private static JobCompleteHelper instance = new JobCompleteHelper(); + public static JobCompleteHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private ThreadPoolExecutor callbackThreadPool = null; + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + + // for callback + callbackThreadPool = new ThreadPoolExecutor( + 2, + 20, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(3000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now)."); + } + }); + + + // for monitor + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // wait for JobTriggerPoolHelper-init + try { + TimeUnit.MILLISECONDS.sleep(50); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + // monitor + while (!toStop) { + try { + // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; + Date losedTime = DateUtil.addMinutes(new Date(), -10); + List losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime); + + if (losedJobIds!=null && losedJobIds.size()>0) { + for (Long logId: losedJobIds) { + + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setId(logId); + + jobLog.setHandleTime(new Date()); + jobLog.setHandleCode(ReturnT.FAIL_CODE); + jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") ); + + XxlJobCompleter.updateHandleInfoAndFinish(jobLog); + } + + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(60); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobLosedMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + callbackThreadPool.shutdownNow(); + + // stop monitorThread (interrupt and wait) + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT callback(List callbackParamList) { + + callbackThreadPool.execute(new Runnable() { + @Override + public void run() { + for (HandleCallbackParam handleCallbackParam: callbackParamList) { + ReturnT callbackResult = callback(handleCallbackParam); + logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}", + (callbackResult.getCode()== ReturnT.SUCCESS_CODE?"success":"fail"), handleCallbackParam, callbackResult); + } + } + }); + + return ReturnT.SUCCESS; + } + + private ReturnT callback(HandleCallbackParam handleCallbackParam) { + // valid log item + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(handleCallbackParam.getLogId()); + if (log == null) { + return new ReturnT(ReturnT.FAIL_CODE, "log item not found."); + } + if (log.getHandleCode() > 0) { + return new ReturnT(ReturnT.FAIL_CODE, "log repeate callback."); // avoid repeat callback, trigger child job etc + } + + // handle msg + StringBuffer handleMsg = new StringBuffer(); + if (log.getHandleMsg()!=null) { + handleMsg.append(log.getHandleMsg()).append("
"); + } + if (handleCallbackParam.getHandleMsg() != null) { + handleMsg.append(handleCallbackParam.getHandleMsg()); + } + + // success, save log + log.setHandleTime(new Date()); + log.setHandleCode(handleCallbackParam.getHandleCode()); + log.setHandleMsg(handleMsg.toString()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + + return ReturnT.SUCCESS; + } + + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java new file mode 100644 index 0000000..8409d7b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java @@ -0,0 +1,110 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * job monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobFailMonitorHelper { + private static Logger logger = LoggerFactory.getLogger(JobFailMonitorHelper.class); + + private static JobFailMonitorHelper instance = new JobFailMonitorHelper(); + public static JobFailMonitorHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // monitor + while (!toStop) { + try { + + List failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000); + if (failLogIds!=null && !failLogIds.isEmpty()) { + for (long failLogId: failLogIds) { + + // lock log + int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1); + if (lockRet < 1) { + continue; + } + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId); + XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId()); + + // 1、fail retry monitor + if (log.getExecutorFailRetryCount() > 0) { + JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); + String retryMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
"; + log.setTriggerMsg(log.getTriggerMsg() + retryMsg); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log); + } + + // 2、fail alarm monitor + int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 + if (info != null) { + boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log); + newAlarmStatus = alarmResult?2:3; + } else { + newAlarmStatus = 1; + } + + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(10); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobFailMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java new file mode 100644 index 0000000..2387a0c --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java @@ -0,0 +1,152 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * job log report helper + * + * @author xuxueli 2019-11-22 + */ +public class JobLogReportHelper { + private static Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class); + + private static JobLogReportHelper instance = new JobLogReportHelper(); + public static JobLogReportHelper getInstance(){ + return instance; + } + + + private Thread logrThread; + private volatile boolean toStop = false; + public void start(){ + logrThread = new Thread(new Runnable() { + + @Override + public void run() { + + // last clean log time + long lastCleanLogTime = 0; + + + while (!toStop) { + + // 1、log-report refresh: refresh log report in 3 days + try { + + for (int i = 0; i < 3; i++) { + + // today + Calendar itemDay = Calendar.getInstance(); + itemDay.add(Calendar.DAY_OF_MONTH, -i); + itemDay.set(Calendar.HOUR_OF_DAY, 0); + itemDay.set(Calendar.MINUTE, 0); + itemDay.set(Calendar.SECOND, 0); + itemDay.set(Calendar.MILLISECOND, 0); + + Date todayFrom = itemDay.getTime(); + + itemDay.set(Calendar.HOUR_OF_DAY, 23); + itemDay.set(Calendar.MINUTE, 59); + itemDay.set(Calendar.SECOND, 59); + itemDay.set(Calendar.MILLISECOND, 999); + + Date todayTo = itemDay.getTime(); + + // refresh log-report every minute + XxlJobLogReport xxlJobLogReport = new XxlJobLogReport(); + xxlJobLogReport.setTriggerDay(todayFrom); + xxlJobLogReport.setRunningCount(0); + xxlJobLogReport.setSucCount(0); + xxlJobLogReport.setFailCount(0); + + Map triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo); + if (triggerCountMap!=null && triggerCountMap.size()>0) { + int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0; + int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0; + int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0; + int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc; + + xxlJobLogReport.setRunningCount(triggerDayCountRunning); + xxlJobLogReport.setSucCount(triggerDayCountSuc); + xxlJobLogReport.setFailCount(triggerDayCountFail); + } + + // do refresh + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e); + } + } + + // 2、log-clean: switch open & once each day + if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0 + && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) { + + // expire-time + Calendar expiredDay = Calendar.getInstance(); + expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays()); + expiredDay.set(Calendar.HOUR_OF_DAY, 0); + expiredDay.set(Calendar.MINUTE, 0); + expiredDay.set(Calendar.SECOND, 0); + expiredDay.set(Calendar.MILLISECOND, 0); + Date clearBeforeTime = expiredDay.getTime(); + + // clean expired log + List logIds = null; + do { + logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000); + if (logIds!=null && logIds.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + // update clean time + lastCleanLogTime = System.currentTimeMillis(); + } + + try { + TimeUnit.MINUTES.sleep(1); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job log report thread stop"); + + } + }); + logrThread.setDaemon(true); + logrThread.setName("xxl-job, admin JobLogReportHelper"); + logrThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + logrThread.interrupt(); + try { + logrThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java new file mode 100644 index 0000000..37edfd9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.concurrent.*; + +/** + * job registry instance + * @author xuxueli 2016-10-02 19:10:24 + */ +public class JobRegistryHelper { + private static Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class); + + private static JobRegistryHelper instance = new JobRegistryHelper(); + public static JobRegistryHelper getInstance(){ + return instance; + } + + private ThreadPoolExecutor registryOrRemoveThreadPool = null; + private Thread registryMonitorThread; + private volatile boolean toStop = false; + + public void start(){ + + // for registry or remove + registryOrRemoveThreadPool = new ThreadPoolExecutor( + 2, + 10, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(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 = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0); + if (groupList!=null && !groupList.isEmpty()) { + + // remove dead address (admin/executor) + List ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (ids!=null && ids.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids); + } + + // fresh online address (admin/executor) + HashMap> appAddressMap = new HashMap>(); + List list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List 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()); + + XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group); + } + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + try { + TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop"); + } + }); + registryMonitorThread.setDaemon(true); + registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread"); + registryMonitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + registryOrRemoveThreadPool.shutdownNow(); + + // stop monitir (interrupt and wait) + registryMonitorThread.interrupt(); + try { + registryMonitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT registry(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + public ReturnT registryRemove(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue()); + if (ret > 0) { + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + private void freshGroupRegistryInfo(RegistryParam registryParam){ + // Under consideration, prevent affecting core tables + } + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java new file mode 100644 index 0000000..2768824 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java @@ -0,0 +1,369 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author xuxueli 2019-05-21 + */ +public class JobScheduleHelper { + private static Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class); + + private static JobScheduleHelper instance = new JobScheduleHelper(); + public static JobScheduleHelper getInstance(){ + return instance; + } + + public static final long PRE_READ_MS = 5000; // pre read + + private Thread scheduleThread; + private Thread ringThread; + private volatile boolean scheduleThreadToStop = false; + private volatile boolean ringThreadToStop = false; + private volatile static Map> ringData = new ConcurrentHashMap<>(); + + public void start(){ + + // schedule thread + scheduleThread = new Thread(new Runnable() { + @Override + public void run() { + + try { + TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + logger.info(">>>>>>>>> init xxl-job admin scheduler success."); + + // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20) + int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; + + while (!scheduleThreadToStop) { + + // Scan Job + long start = System.currentTimeMillis(); + + Connection conn = null; + Boolean connAutoCommit = null; + PreparedStatement preparedStatement = null; + + boolean preReadSuc = true; + try { + + conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); + connAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); + preparedStatement.execute(); + + // tx start + + // 1、pre read + long nowTime = System.currentTimeMillis(); + List scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount); + if (scheduleList!=null && scheduleList.size()>0) { + // 2、push time-ring + for (XxlJobInfo jobInfo: scheduleList) { + + // time-ring jump + if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) { + // 2.1、trigger-expire > 5s:pass && make next-trigger-time + logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId()); + + // 1、misfire match + MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); + if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) { + // FIRE_ONCE_NOW 》 trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + } + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + } else if (nowTime > jobInfo.getTriggerNextTime()) { + // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time + + // 1、trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + // next-trigger-time in 5s, pre-read again + if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) { + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } else { + // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } + + // 3、update trigger info + for (XxlJobInfo jobInfo: scheduleList) { + XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); + } + + } else { + preReadSuc = false; + } + + // tx stop + + + } catch (Exception e) { + if (!scheduleThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e); + } + } finally { + + // commit + if (conn != null) { + try { + conn.commit(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.setAutoCommit(connAutoCommit); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + // close PreparedStatement + if (null != preparedStatement) { + try { + preparedStatement.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + } + long cost = System.currentTimeMillis()-start; + + + // Wait seconds, align second + if (cost < 1000) { // scan-overtime, not wait + try { + // pre-read period: success > scan each second; fail > skip this period; + TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop"); + } + }); + scheduleThread.setDaemon(true); + scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread"); + scheduleThread.start(); + + + // ring thread + ringThread = new Thread(new Runnable() { + @Override + public void run() { + + while (!ringThreadToStop) { + + // align second + try { + TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + if (!ringThreadToStop) { + logger.error(e.getMessage(), e); + } + } + + try { + // second data + List ringItemData = new ArrayList<>(); + int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长,跨过刻度,向前校验一个刻度; + for (int i = 0; i < 2; i++) { + List tmpData = ringData.remove( (nowSecond+60-i)%60 ); + if (tmpData != null) { + ringItemData.addAll(tmpData); + } + } + + // ring trigger + logger.trace(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) ); + if (ringItemData.size() > 0) { + // do trigger + for (int jobId: ringItemData) { + // do trigger + JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null); + } + // clear + ringItemData.clear(); + } + } catch (Exception e) { + if (!ringThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop"); + } + }); + ringThread.setDaemon(true); + ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread"); + ringThread.start(); + } + + private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + Date nextValidTime = generateNextValidTime(jobInfo, fromTime); + if (nextValidTime != null) { + jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime()); + jobInfo.setTriggerNextTime(nextValidTime.getTime()); + } else { + jobInfo.setTriggerStatus(0); + jobInfo.setTriggerLastTime(0); + jobInfo.setTriggerNextTime(0); + logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}", + jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf()); + } + } + + private void pushTimeRing(int ringSecond, int jobId){ + // push async ring + List ringItemData = ringData.get(ringSecond); + if (ringItemData == null) { + ringItemData = new ArrayList(); + ringData.put(ringSecond, ringItemData); + } + ringItemData.add(jobId); + + logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) ); + } + + public void toStop(){ + + // 1、stop schedule + scheduleThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); // wait + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (scheduleThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + scheduleThread.interrupt(); + try { + scheduleThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // if has ring data + boolean hasRingData = false; + if (!ringData.isEmpty()) { + for (int second : ringData.keySet()) { + List tmpData = ringData.get(second); + if (tmpData!=null && tmpData.size()>0) { + hasRingData = true; + break; + } + } + } + if (hasRingData) { + try { + TimeUnit.SECONDS.sleep(8); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // stop ring (wait job-in-memory stop) + ringThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (ringThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + ringThread.interrupt(); + try { + ringThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop"); + } + + + // ---------------------- tools ---------------------- + public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (ScheduleTypeEnum.CRON == scheduleTypeEnum) { + Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime); + return nextValidTime; + } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) { + return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 ); + } + return null; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java new file mode 100644 index 0000000..398713d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java @@ -0,0 +1,150 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.trigger.XxlJobTrigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * job trigger thread pool helper + * + * @author xuxueli 2018-07-03 21:08:07 + */ +public class JobTriggerPoolHelper { + private static Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class); + + + // ---------------------- trigger pool ---------------------- + + // fast/slow thread pool + private ThreadPoolExecutor fastTriggerPool = null; + private ThreadPoolExecutor slowTriggerPool = null; + + public void start(){ + fastTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(1000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()); + } + }); + + slowTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()); + } + }); + } + + + public void stop() { + //triggerPool.shutdown(); + fastTriggerPool.shutdownNow(); + slowTriggerPool.shutdownNow(); + logger.info(">>>>>>>>> xxl-job trigger thread pool shutdown success."); + } + + + // job timeout count + private volatile long minTim = System.currentTimeMillis()/60000; // ms > min + private volatile ConcurrentMap jobTimeoutCountMap = new ConcurrentHashMap<>(); + + + /** + * add trigger + */ + public void addTrigger(final int jobId, + final TriggerTypeEnum triggerType, + final int failRetryCount, + final String executorShardingParam, + final String executorParam, + final String addressList) { + + // choose thread pool + ThreadPoolExecutor triggerPool_ = fastTriggerPool; + AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId); + if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job-timeout 10 times in 1 min + triggerPool_ = slowTriggerPool; + } + + // trigger + triggerPool_.execute(new Runnable() { + @Override + public void run() { + + long start = System.currentTimeMillis(); + + try { + // do trigger + XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + + // check timeout-count-map + long minTim_now = System.currentTimeMillis()/60000; + if (minTim != minTim_now) { + minTim = minTim_now; + jobTimeoutCountMap.clear(); + } + + // incr timeout-count-map + long cost = System.currentTimeMillis()-start; + if (cost > 500) { // ob-timeout threshold 500ms + AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1)); + if (timeoutCount != null) { + timeoutCount.incrementAndGet(); + } + } + + } + + } + }); + } + + + + // ---------------------- helper ---------------------- + + private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper(); + + public static void toStart() { + helper.start(); + } + public static void toStop() { + helper.stop(); + } + + /** + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + */ + public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { + helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java new file mode 100644 index 0000000..446c90e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java @@ -0,0 +1,27 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * trigger type enum + * + * @author xuxueli 2018-09-16 04:56:41 + */ +public enum TriggerTypeEnum { + + MANUAL(I18nUtil.getString("jobconf_trigger_type_manual")), + CRON(I18nUtil.getString("jobconf_trigger_type_cron")), + RETRY(I18nUtil.getString("jobconf_trigger_type_retry")), + PARENT(I18nUtil.getString("jobconf_trigger_type_parent")), + API(I18nUtil.getString("jobconf_trigger_type_api")), + MISFIRE(I18nUtil.getString("jobconf_trigger_type_misfire")); + + private TriggerTypeEnum(String title){ + this.title = title; + } + private String title; + public String getTitle() { + return title; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java new file mode 100644 index 0000000..748befc --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java @@ -0,0 +1,226 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.util.IpUtil; +import com.xxl.job.core.util.ThrowableUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * xxl-job trigger + * Created by xuxueli on 17/7/13. + */ +public class XxlJobTrigger { + private static Logger logger = LoggerFactory.getLogger(XxlJobTrigger.class); + + /** + * trigger job + * + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + * @param addressList + * null: use executor addressList + * not null: cover + */ + public static void trigger(int jobId, + TriggerTypeEnum triggerType, + int failRetryCount, + String executorShardingParam, + String executorParam, + String addressList) { + + // load data + XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId); + if (jobInfo == null) { + logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId); + return; + } + if (executorParam != null) { + jobInfo.setExecutorParam(executorParam); + } + int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount(); + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup()); + + // cover addressList + if (addressList!=null && addressList.trim().length()>0) { + group.setAddressType(1); + group.setAddressList(addressList.trim()); + } + + // sharding param + int[] shardingParam = null; + if (executorShardingParam!=null){ + String[] shardingArr = executorShardingParam.split("/"); + if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) { + shardingParam = new int[2]; + shardingParam[0] = Integer.valueOf(shardingArr[0]); + shardingParam[1] = Integer.valueOf(shardingArr[1]); + } + } + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) + && group.getRegistryList()!=null && !group.getRegistryList().isEmpty() + && shardingParam==null) { + for (int i = 0; i < group.getRegistryList().size(); i++) { + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size()); + } + } else { + if (shardingParam == null) { + shardingParam = new int[]{0, 1}; + } + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]); + } + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * @param group job group, registry list may be empty + * @param jobInfo + * @param finalFailRetryCount + * @param triggerType + * @param index sharding index + * @param total sharding index + */ + private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){ + + // param + ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy + ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy + String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null; + + // 1、save log-id + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setJobGroup(jobInfo.getJobGroup()); + jobLog.setJobId(jobInfo.getId()); + jobLog.setTriggerTime(new Date()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog); + logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId()); + + // 2、init trigger-param + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setJobId(jobInfo.getId()); + triggerParam.setExecutorHandler(jobInfo.getExecutorHandler()); + triggerParam.setExecutorParams(jobInfo.getExecutorParam()); + triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout()); + triggerParam.setLogId(jobLog.getId()); + triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime()); + triggerParam.setGlueType(jobInfo.getGlueType()); + triggerParam.setGlueSource(jobInfo.getGlueSource()); + triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime()); + triggerParam.setBroadcastIndex(index); + triggerParam.setBroadcastTotal(total); + + // 3、init address + String address = null; + ReturnT routeAddressResult = null; + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { + if (index < group.getRegistryList().size()) { + address = group.getRegistryList().get(index); + } else { + address = group.getRegistryList().get(0); + } + } else { + routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList()); + if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) { + address = routeAddressResult.getContent(); + } + } + } else { + routeAddressResult = new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty")); + } + + // 4、trigger remote executor + ReturnT triggerResult = null; + if (address != null) { + triggerResult = runExecutor(triggerParam, address); + } else { + triggerResult = new ReturnT(ReturnT.FAIL_CODE, null); + } + + // 5、collection trigger info + StringBuffer triggerMsgSb = new StringBuffer(); + triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.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("("+shardingParam+")"); + } + 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); + + triggerMsgSb.append("

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<<
") + .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"

":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():""); + + // 6、save log trigger-info + jobLog.setExecutorAddress(address); + jobLog.setExecutorHandler(jobInfo.getExecutorHandler()); + jobLog.setExecutorParam(jobInfo.getExecutorParam()); + jobLog.setExecutorShardingParam(shardingParam); + jobLog.setExecutorFailRetryCount(finalFailRetryCount); + //jobLog.setTriggerTime(); + jobLog.setTriggerCode(triggerResult.getCode()); + jobLog.setTriggerMsg(triggerMsgSb.toString()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog); + + logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId()); + } + + /** + * run executor + * @param triggerParam + * @param address + * @return + */ + public static ReturnT runExecutor(TriggerParam triggerParam, String address){ + ReturnT runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + runResult = executorBiz.run(triggerParam); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e); + runResult = new ReturnT(ReturnT.FAIL_CODE, ThrowableUtil.toString(e)); + } + + 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()); + + runResult.setMsg(runResultSB.toString()); + return runResult; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java new file mode 100644 index 0000000..a1523aa --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java @@ -0,0 +1,98 @@ +package com.xxl.job.admin.core.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Cookie.Util + * + * @author xuxueli 2015-12-12 18:01:06 + */ +public class CookieUtil { + + // 默认缓存时间,单位/秒, 2H + private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE; + // 保存路径,根路径 + private static final String COOKIE_PATH = "/"; + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param ifRemember + */ + public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) { + int age = ifRemember?COOKIE_MAX_AGE:-1; + set(response, key, value, null, COOKIE_PATH, age, true); + } + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param maxAge + */ + private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) { + Cookie cookie = new Cookie(key, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(isHttpOnly); + response.addCookie(cookie); + } + + /** + * 查询value + * + * @param request + * @param key + * @return + */ + public static String getValue(HttpServletRequest request, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + /** + * 查询Cookie + * + * @param request + * @param key + */ + private static Cookie get(HttpServletRequest request, String key) { + Cookie[] arr_cookie = request.getCookies(); + if (arr_cookie != null && arr_cookie.length > 0) { + for (Cookie cookie : arr_cookie) { + if (cookie.getName().equals(key)) { + return cookie; + } + } + } + return null; + } + + /** + * 删除Cookie + * + * @param request + * @param response + * @param key + */ + public static void remove(HttpServletRequest request, HttpServletResponse response, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + set(response, key, "", null, COOKIE_PATH, 0, true); + } + } + +} \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java new file mode 100644 index 0000000..e90af43 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.core.util; + +import freemarker.ext.beans.BeansWrapper; +import freemarker.ext.beans.BeansWrapperBuilder; +import freemarker.template.Configuration; +import freemarker.template.TemplateHashModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ftl util + * + * @author xuxueli 2018-01-17 20:37:48 + */ +public class FtlUtil { + private static Logger logger = LoggerFactory.getLogger(FtlUtil.class); + + private static BeansWrapper wrapper = new BeansWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build(); //BeansWrapper.getDefaultInstance(); + + public static TemplateHashModel generateStaticModel(String packageName) { + try { + TemplateHashModel staticModels = wrapper.getStaticModels(); + TemplateHashModel fileStatics = (TemplateHashModel) staticModels.get(packageName); + return fileStatics; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java new file mode 100644 index 0000000..772a96e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.util; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * i18n util + * + * @author xuxueli 2018-01-17 20:39:06 + */ +public class I18nUtil { + private static Logger logger = LoggerFactory.getLogger(I18nUtil.class); + + private static Properties prop = null; + public static Properties loadI18nProp(){ + if (prop != null) { + return prop; + } + try { + // build i18n prop + String i18n = XxlJobAdminConfig.getAdminConfig().getI18n(); + String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n); + + // load prop + Resource resource = new ClassPathResource(i18nFile); + EncodedResource encodedResource = new EncodedResource(resource,"UTF-8"); + prop = PropertiesLoaderUtils.loadProperties(encodedResource); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return prop; + } + + /** + * get val of i18n key + * + * @param key + * @return + */ + public static String getString(String key) { + return loadI18nProp().getProperty(key); + } + + /** + * get mult val of i18n mult key, as json + * + * @param keys + * @return + */ + public static String getMultString(String... keys) { + Map 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)); + } + } + + String json = JacksonUtil.writeValueAsString(map); + return json; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java new file mode 100644 index 0000000..4f4ea3c --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java @@ -0,0 +1,92 @@ +package com.xxl.job.admin.core.util; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Jackson util + * + * 1、obj need private and set/get; + * 2、do not support inner class; + * + * @author xuxueli 2015-9-25 18:02:56 + */ +public class JacksonUtil { + private static Logger logger = LoggerFactory.getLogger(JacksonUtil.class); + + private final static ObjectMapper objectMapper = new ObjectMapper(); + public static ObjectMapper getInstance() { + return objectMapper; + } + + /** + * bean、array、List、Map --> json + * + * @param obj + * @return json string + * @throws Exception + */ + public static String writeValueAsString(Object obj) { + try { + return getInstance().writeValueAsString(obj); + } catch (JsonGenerationException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> bean、Map、List(array) + * + * @param jsonStr + * @param clazz + * @return obj + * @throws Exception + */ + public static T 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; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java new file mode 100644 index 0000000..fbab061 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java @@ -0,0 +1,133 @@ +package com.xxl.job.admin.core.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * local cache tool + * + * @author xuxueli 2018-01-22 21:37:34 + */ +public class LocalCacheUtil { + + private static ConcurrentMap 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; + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java new file mode 100644 index 0000000..b608d9f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java @@ -0,0 +1,37 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobGroupDao { + + public List 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); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java new file mode 100644 index 0000000..d640eff --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java @@ -0,0 +1,49 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +/** + * job info + * @author xuxueli 2016-1-12 18:03:45 + */ +@Mapper +public interface XxlJobInfoDao { + + public List 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(); + + public List scheduleJobQuery(@Param("maxNextTime") long maxNextTime, @Param("pagesize") int pagesize ); + + public int scheduleUpdate(XxlJobInfo xxlJobInfo); + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java new file mode 100644 index 0000000..62fa3b4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java @@ -0,0 +1,62 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * job log + * @author xuxueli 2016-1-12 18:03:06 + */ +@Mapper +public interface XxlJobLogDao { + + // exist jobId not use jobGroup, not exist use jobGroup + public List 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); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java new file mode 100644 index 0000000..3028aed --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * job log for glue + * @author xuxueli 2016-5-19 18:04:56 + */ +@Mapper +public interface XxlJobLogGlueDao { + + public int save(XxlJobLogGlue xxlJobLogGlue); + + public List findByJobId(@Param("jobId") int jobId); + + public int removeOld(@Param("jobId") int jobId, @Param("limit") int limit); + + public int deleteByJobId(@Param("jobId") int jobId); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java new file mode 100644 index 0000000..f4b3dc8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java @@ -0,0 +1,26 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * job log + * @author xuxueli 2019-11-22 + */ +@Mapper +public interface XxlJobLogReportDao { + + public int save(XxlJobLogReport xxlJobLogReport); + + public int update(XxlJobLogReport xxlJobLogReport); + + public List queryLogReport(@Param("triggerDayFrom") Date triggerDayFrom, + @Param("triggerDayTo") Date triggerDayTo); + + public XxlJobLogReport queryLogReportTotal(); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java new file mode 100644 index 0000000..1005c46 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java @@ -0,0 +1,38 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobRegistry; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobRegistryDao { + + public List 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 registryUpdate(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registrySave(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registryDelete(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java new file mode 100644 index 0000000..e840494 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * @author xuxueli 2019-05-04 16:44:59 + */ +@Mapper +public interface XxlJobUserDao { + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + + public XxlJobUser loadByUserName(@Param("username") String username); + + public int save(XxlJobUser xxlJobUser); + + public int update(XxlJobUser xxlJobUser); + + public int delete(@Param("id") int id); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java new file mode 100644 index 0000000..e1cf2e4 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java @@ -0,0 +1,107 @@ +package com.xxl.job.admin.service; + +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.CookieUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.core.util.JacksonUtil; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.DigestUtils; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.math.BigInteger; + +/** + * @author xuxueli 2019-05-04 22:13:264 + */ +@Configuration +public class LoginService { + + public static final String LOGIN_IDENTITY_KEY = "XXL_JOB_LOGIN_IDENTITY"; + + @Resource + private XxlJobUserDao xxlJobUserDao; + + + private String makeToken(XxlJobUser xxlJobUser){ + String tokenJson = JacksonUtil.writeValueAsString(xxlJobUser); + String tokenHex = new BigInteger(tokenJson.getBytes()).toString(16); + return tokenHex; + } + private XxlJobUser parseToken(String tokenHex){ + XxlJobUser xxlJobUser = null; + if (tokenHex != null) { + String tokenJson = new String(new BigInteger(tokenHex, 16).toByteArray()); // username_password(md5) + xxlJobUser = JacksonUtil.readValue(tokenJson, XxlJobUser.class); + } + return xxlJobUser; + } + + + public ReturnT login(HttpServletRequest request, HttpServletResponse response, String username, String password, boolean ifRemember){ + + // param + if (username==null || username.trim().length()==0 || password==null || password.trim().length()==0){ + return new ReturnT(500, I18nUtil.getString("login_param_empty")); + } + + // valid passowrd + XxlJobUser xxlJobUser = xxlJobUserDao.loadByUserName(username); + if (xxlJobUser == null) { + return new ReturnT(500, I18nUtil.getString("login_param_unvalid")); + } + String passwordMd5 = DigestUtils.md5DigestAsHex(password.getBytes()); + if (!passwordMd5.equals(xxlJobUser.getPassword())) { + return new ReturnT(500, I18nUtil.getString("login_param_unvalid")); + } + + String loginToken = makeToken(xxlJobUser); + + // do login + CookieUtil.set(response, LOGIN_IDENTITY_KEY, loginToken, ifRemember); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @param response + */ + public ReturnT logout(HttpServletRequest request, HttpServletResponse response){ + CookieUtil.remove(request, response, LOGIN_IDENTITY_KEY); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @return + */ + public XxlJobUser ifLogin(HttpServletRequest request, HttpServletResponse response){ + String cookieToken = CookieUtil.getValue(request, LOGIN_IDENTITY_KEY); + if (cookieToken != null) { + XxlJobUser cookieUser = null; + try { + cookieUser = parseToken(cookieToken); + } catch (Exception e) { + logout(request, response); + } + if (cookieUser != null) { + XxlJobUser dbUser = xxlJobUserDao.loadByUserName(cookieUser.getUsername()); + if (dbUser != null) { + if (cookieUser.getPassword().equals(dbUser.getPassword())) { + return dbUser; + } + } + } + } + return null; + } + + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java new file mode 100644 index 0000000..60b4bb8 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java @@ -0,0 +1,98 @@ +package com.xxl.job.admin.service; + + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.core.biz.model.ReturnT; + +import java.util.Date; +import java.util.Map; + +/** + * core job action for xxl-job + * + * @author xuxueli 2016-5-28 15:30:33 + */ +public interface XxlJobService { + + /** + * page list + * + * @param start + * @param length + * @param jobGroup + * @param jobDesc + * @param executorHandler + * @param author + * @return + */ + public Map pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author); + + /** + * add job + * + * @param jobInfo + * @return + */ + public ReturnT add(XxlJobInfo jobInfo); + + /** + * update job + * + * @param jobInfo + * @return + */ + public ReturnT update(XxlJobInfo jobInfo); + + /** + * remove job + * * + * @param id + * @return + */ + public ReturnT remove(int id); + + /** + * start job + * + * @param id + * @return + */ + public ReturnT start(int id); + + /** + * stop job + * + * @param id + * @return + */ + public ReturnT stop(int id); + + /** + * trigger + * + * @param loginUser + * @param jobId + * @param executorParam + * @param addressList + * @return + */ + public ReturnT trigger(XxlJobUser loginUser, int jobId, String executorParam, String addressList); + + /** + * dashboard info + * + * @return + */ + public Map dashboardInfo(); + + /** + * chart info + * + * @param startDate + * @param endDate + * @return + */ + public ReturnT> chartInfo(Date startDate, Date endDate); + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java new file mode 100644 index 0000000..3c01e94 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java @@ -0,0 +1,35 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.thread.JobCompleteHelper; +import com.xxl.job.admin.core.thread.JobRegistryHelper; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author xuxueli 2017-07-27 21:54:20 + */ +@Service +public class AdminBizImpl implements AdminBiz { + + + @Override + public ReturnT callback(List callbackParamList) { + return JobCompleteHelper.getInstance().callback(callbackParamList); + } + + @Override + public ReturnT registry(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registry(registryParam); + } + + @Override + public ReturnT registryRemove(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registryRemove(registryParam); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java new file mode 100644 index 0000000..b7d9688 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java @@ -0,0 +1,473 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.*; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.text.MessageFormat; +import java.util.*; + +/** + * core job action for xxl-job + * @author xuxueli 2016-5-28 15:30:33 + */ +@Service +public class XxlJobServiceImpl implements XxlJobService { + private static Logger logger = LoggerFactory.getLogger(XxlJobServiceImpl.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + + @Override + public Map pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + // page list + List list = xxlJobInfoDao.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + int list_count = xxlJobInfoDao.pageListCount(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @Override + public ReturnT add(XxlJobInfo jobInfo) { + + // valid base + XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (group == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")) ); + } + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid")); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid job + if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) ); + } + if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+"JobHandler") ); + } + // 》fix "\r" in shell + if (GlueTypeEnum.GLUE_SHELL==GlueTypeEnum.match(jobInfo.getGlueType()) && jobInfo.getGlueSource()!=null) { + jobInfo.setGlueSource(jobInfo.getGlueSource().replaceAll("\r", "")); + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // add in db + jobInfo.setAddTime(new Date()); + jobInfo.setUpdateTime(new Date()); + jobInfo.setGlueUpdatetime(new Date()); + xxlJobInfoDao.save(jobInfo); + if (jobInfo.getId() < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) ); + } + + return new ReturnT(String.valueOf(jobInfo.getId())); + } + + private boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + public ReturnT update(XxlJobInfo jobInfo) { + + // valid base + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid") ); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE /*|| scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // group valid + XxlJobGroup jobGroup = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (jobGroup == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_jobgroup")+I18nUtil.getString("system_unvalid")) ); + } + + // stage job info + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId()); + if (exists_jobInfo == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = exists_jobInfo.getTriggerNextTime(); + boolean scheduleDataNotChanged = jobInfo.getScheduleType().equals(exists_jobInfo.getScheduleType()) && jobInfo.getScheduleConf().equals(exists_jobInfo.getScheduleConf()); + if (exists_jobInfo.getTriggerStatus() == 1 && !scheduleDataNotChanged) { + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(jobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + exists_jobInfo.setJobGroup(jobInfo.getJobGroup()); + exists_jobInfo.setJobDesc(jobInfo.getJobDesc()); + exists_jobInfo.setAuthor(jobInfo.getAuthor()); + exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail()); + exists_jobInfo.setScheduleType(jobInfo.getScheduleType()); + exists_jobInfo.setScheduleConf(jobInfo.getScheduleConf()); + exists_jobInfo.setMisfireStrategy(jobInfo.getMisfireStrategy()); + exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy()); + exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler()); + exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam()); + exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout()); + exists_jobInfo.setExecutorFailRetryCount(jobInfo.getExecutorFailRetryCount()); + exists_jobInfo.setChildJobId(jobInfo.getChildJobId()); + exists_jobInfo.setTriggerNextTime(nextTriggerTime); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + + return ReturnT.SUCCESS; + } + + @Override + public ReturnT remove(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + if (xxlJobInfo == null) { + return ReturnT.SUCCESS; + } + + xxlJobInfoDao.delete(id); + xxlJobLogDao.delete(id); + xxlJobLogGlueDao.deleteByJobId(id); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT start(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + // valid + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE); + if (ScheduleTypeEnum.NONE == scheduleTypeEnum) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type_none_limit_start")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = 0; + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(xxlJobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + + xxlJobInfo.setTriggerStatus(1); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(nextTriggerTime); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT stop(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + xxlJobInfo.setTriggerStatus(0); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(0); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + + + @Override + public ReturnT trigger(XxlJobUser loginUser, int jobId, String executorParam, String addressList) { + // permission + if (loginUser == null) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("system_permission_limit")); + } + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(jobId); + if (xxlJobInfo == null) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (!hasPermission(loginUser, xxlJobInfo.getJobGroup())) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("system_permission_limit")); + } + + // force cover job param + if (executorParam == null) { + executorParam = ""; + } + + JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); + return ReturnT.SUCCESS; + } + + private boolean hasPermission(XxlJobUser loginUser, int jobGroup){ + if (loginUser.getRole() == 1) { + return true; + } + List groupIdStrs = new ArrayList<>(); + if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) { + groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(",")); + } + return groupIdStrs.contains(String.valueOf(jobGroup)); + } + + @Override + public Map dashboardInfo() { + + int jobInfoCount = xxlJobInfoDao.findAllCount(); + int jobLogCount = 0; + int jobLogSuccessCount = 0; + XxlJobLogReport xxlJobLogReport = xxlJobLogReportDao.queryLogReportTotal(); + if (xxlJobLogReport != null) { + jobLogCount = xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount(); + jobLogSuccessCount = xxlJobLogReport.getSucCount(); + } + + // executor count + Set executorAddressSet = new HashSet(); + List groupList = xxlJobGroupDao.findAll(); + + if (groupList!=null && !groupList.isEmpty()) { + for (XxlJobGroup group: groupList) { + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + executorAddressSet.addAll(group.getRegistryList()); + } + } + } + + int executorCount = executorAddressSet.size(); + + Map dashboardMap = new HashMap(); + dashboardMap.put("jobInfoCount", jobInfoCount); + dashboardMap.put("jobLogCount", jobLogCount); + dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount); + dashboardMap.put("executorCount", executorCount); + return dashboardMap; + } + + @Override + public ReturnT> 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 = xxlJobLogReportDao.queryLogReport(startDate, endDate); + + if (logReportList!=null && logReportList.size()>0) { + for (XxlJobLogReport item: logReportList) { + String day = DateUtil.formatDate(item.getTriggerDay()); + int triggerDayCountRunning = item.getRunningCount(); + int triggerDayCountSuc = item.getSucCount(); + int triggerDayCountFail = item.getFailCount(); + + triggerDayList.add(day); + triggerDayCountRunningList.add(triggerDayCountRunning); + triggerDayCountSucList.add(triggerDayCountSuc); + triggerDayCountFailList.add(triggerDayCountFail); + + triggerCountRunningTotal += triggerDayCountRunning; + triggerCountSucTotal += triggerDayCountSuc; + triggerCountFailTotal += triggerDayCountFail; + } + } else { + for (int i = -6; i <= 0; i++) { + triggerDayList.add(DateUtil.formatDate(DateUtil.addDays(new Date(), i))); + triggerDayCountRunningList.add(0); + triggerDayCountSucList.add(0); + triggerDayCountFailList.add(0); + } + } + + Map 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 new ReturnT>(result); + } + +} diff --git a/xxl-job/xxl-job-admin/src/main/resources/application.yml b/xxl-job/xxl-job-admin/src/main/resources/application.yml new file mode 100644 index 0000000..45d3f53 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/application.yml @@ -0,0 +1,78 @@ +server: + port: 10003 + servlet: + context-path: /xxl-job-admin +management: + server: + base-path: /actuator + health: + mail: + enabled: false + +spring: + application: + name: @artifactId@ + profiles: + active: ${APP_PROFILE:dev} + mvc: + servlet: + load-on-startup: 0 + static-path-pattern: /static/** + web: + resources: + static-locations: classpath:/static/ + freemarker: + templateLoaderPath: classpath:/templates/ + suffix: .ftl + charset: UTF-8 + request-context-attribute: request + settings: + number_format: 0.########## + new_builtin_class_resolver: safer + datasource: + url: jdbc:mysql://localhost:3306/njzscloud_supervisory?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + type: com.zaxxer.hikari.HikariDataSource + hikari: + minimum-idle: 10 + maximum-pool-size: 30 + auto-commit: true + idle-timeout: 30000 + pool-name: HikariCP + max-lifetime: 900000 + connection-timeout: 10000 + connection-test-query: SELECT 1 + validation-timeout: 1000 + mail: + host: smtp.qq.com + port: 25 + username: xxx@qq.com + from: xxx@qq.com + password: xxx + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + socketFactory: + class: javax.net.ssl.SSLSocketFactory +mybatis: + mapper-locations: classpath:/mybatis-mapper/*Mapper.xml + #type-aliases-package: com.xxl.job.admin.core.model +xxl: + job: + accessToken: default_token + logretentiondays: 30 + i18n: zh_CN + triggerpool: + fast: + max: 200 + slow: + max: 100 +logging: + level: + com.xxl: debug diff --git a/xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties new file mode 100644 index 0000000..89dea5f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties @@ -0,0 +1,276 @@ +admin_name = Scheduling Center +admin_name_full = Distributed Task Scheduling Platform XXL-JOB +admin_version = 2.4.1 +admin_i18n = en + +## system +system_tips = System message +system_ok = Confirm +system_close = Close +system_save = Save +system_cancel = Cancel +system_search = Search +system_status = Status +system_opt = Operate +system_please_input = please input +system_please_choose = please choose +system_success = success +system_fail = fail +system_add_suc = add success +system_add_fail = add fail +system_update_suc = update success +system_update_fail = update fail +system_all = All +system_api_error = net error +system_show = Show +system_empty = Empty +system_opt_suc = operate success +system_opt_fail = operate fail +system_opt_edit = Edit +system_opt_del = Delete +system_opt_copy = Copy +system_unvalid = illegal +system_not_found = not exist +system_nav = Navigation +system_digits = digits +system_lengh_limit = Length limit +system_permission_limit = Permission limit +system_welcome = Welcome + +## daterangepicker +daterangepicker_ranges_recent_hour = recent one hour +daterangepicker_ranges_today = today +daterangepicker_ranges_yesterday = yesterday +daterangepicker_ranges_this_month = this month +daterangepicker_ranges_last_month = last month +daterangepicker_ranges_recent_week = recent one week +daterangepicker_ranges_recent_month = recent one month +daterangepicker_custom_name = custom +daterangepicker_custom_starttime = start time +daterangepicker_custom_endtime = end time +daterangepicker_custom_daysofweek = Sun,Mon,Tue,Wed,Thu,Fri,Sat +daterangepicker_custom_monthnames = Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec + +## dataTable +dataTable_sProcessing = processing... +dataTable_sLengthMenu = _MENU_ records per page +dataTable_sZeroRecords = No matching results +dataTable_sInfo = page _PAGE_ ( Total _PAGES_ pages,_TOTAL_ records ) +dataTable_sInfoEmpty = No Record +dataTable_sInfoFiltered = (Filtered by _MAX_ results) +dataTable_sSearch = Search +dataTable_sEmptyTable = Table data is empty +dataTable_sLoadingRecords = Loading... +dataTable_sFirst = FIRST PAGE +dataTable_sPrevious = Previous Page +dataTable_sNext = Next Page +dataTable_sLast = LAST PAGE +dataTable_sSortAscending = : Rank this column in ascending order +dataTable_sSortDescending = : Rank this column in descending order + +## login +login_btn = Login +login_remember_me = Remember Me +login_username_placeholder = Please enter username +login_password_placeholder = Please enter password +login_username_empty = Please enter username +login_username_lt_4 = Username length should not be less than 4 +login_password_empty = Please enter password +login_password_lt_4 = Password length should not be less than 4 +login_success = Login success +login_fail = Login fail +login_param_empty = Username or password is empty +login_param_unvalid = Username or password error + +## logout +logout_btn = Logout +logout_confirm = Confirm logout? +logout_success = Logout success +logout_fail = Logout fail + +## change pwd +change_pwd = Change password +change_pwd_suc_to_logout = Change password successful, about to log out login +change_pwd_field_newpwd = new password + +## dashboard +job_dashboard_name = Run report +job_dashboard_job_num = Job number +job_dashboard_job_num_tip = The number of tasks running in the scheduling center +job_dashboard_trigger_num = trigger number +job_dashboard_trigger_num_tip = The number of trigger record scheduled by the scheduling center +job_dashboard_jobgroup_num = Executor number +job_dashboard_jobgroup_num_tip = The number of online executor machines perceived by the scheduling center +job_dashboard_report = Scheduling report +job_dashboard_report_loaddata_fail = Scheduling report load data error +job_dashboard_date_report = Date distribution +job_dashboard_rate_report = Percentage distribution + +## job info +jobinfo_name = Job Manage +jobinfo_job = Job +jobinfo_field_add = Add Job +jobinfo_field_update = Edit Job +jobinfo_field_id = Job ID +jobinfo_field_jobgroup = Executor +jobinfo_field_jobdesc = Job description +jobinfo_field_timeout = Job timeout period +jobinfo_field_gluetype = GLUE Type +jobinfo_field_executorparam = Param +jobinfo_field_author = Author +jobinfo_field_alarmemail = Alarm email +jobinfo_field_alarmemail_placeholder = Please enter alarm mail, if there are more than one comma separated +jobinfo_field_executorRouteStrategy = Route Strategy +jobinfo_field_childJobId = Child Job ID +jobinfo_field_childJobId_placeholder = Please enter the Child job ID, if there are more than one comma separated +jobinfo_field_executorBlockStrategy = Block Strategy +jobinfo_field_executorFailRetryCount = Fail Retry Count +jobinfo_field_executorFailRetryCount_placeholder = Fail Retry Count. effect if greater than zero +jobinfo_script_location = Script location +jobinfo_shard_index = Shard index +jobinfo_shard_total = Shard total +jobinfo_opt_stop = Stop +jobinfo_opt_start = Start +jobinfo_opt_log = Query Log +jobinfo_opt_run = Run Once +jobinfo_opt_run_tips = Please input the address for this trigger. Null will be obtained from the executor +jobinfo_opt_registryinfo = Registry Info +jobinfo_opt_next_time = Next trigger time +jobinfo_glue_remark = Resource Remark +jobinfo_glue_remark_limit = Resource Remark length is limited to 4~100 +jobinfo_glue_rollback = Version Backtrack +jobinfo_glue_jobid_unvalid = Job ID is illegal +jobinfo_glue_gluetype_unvalid = The job is not GLUE Type +jobinfo_field_executorTimeout_placeholder = Job Timeout period,in seconds. effect if greater than zero +schedule_type = Schedule Type +schedule_type_none = None +schedule_type_cron = Cron +schedule_type_fix_rate = Fix rate +schedule_type_fix_delay = Fix delay +schedule_type_none_limit_start = The current schedule type disables startup +misfire_strategy = Misfire strategy +misfire_strategy_do_nothing = Do nothing +misfire_strategy_fire_once_now = Fire once now +jobinfo_conf_base = Base configuration +jobinfo_conf_schedule = Schedule configuration +jobinfo_conf_job = Job configuration +jobinfo_conf_advanced = Advanced configuration + +## job log +joblog_name = Trigger Log +joblog_status = Status +joblog_status_all = All +joblog_status_suc = Success +joblog_status_fail = Fail +joblog_status_running = Running +joblog_field_triggerTime = Trigger Time +joblog_field_triggerCode = Trigger Result +joblog_field_triggerMsg = Trigger Msg +joblog_field_handleTime = Handle Time +joblog_field_handleCode = Handle Result +joblog_field_handleMsg = Trigger Msg +joblog_field_executorAddress = Executor Address +joblog_clean = Clean +joblog_clean_log = Clean Log +joblog_clean_type = Clean Type +joblog_clean_type_1 = Clean up log data a month ago +joblog_clean_type_2 = Clean up log data three month ago +joblog_clean_type_3 = Clean up log data six month ago +joblog_clean_type_4 = Clean up log data a year ago +joblog_clean_type_5 = Clean up log data a thousand record ago +joblog_clean_type_6 = Clean up log data ten thousand record ago +joblog_clean_type_7 = Clean up log data thirty thousand record ago +joblog_clean_type_8 = Clean up log data hundred thousand record ago +joblog_clean_type_9 = Clean up all log data +joblog_clean_type_unvalid = Clean type is illegal +joblog_handleCode_200 = Success +joblog_handleCode_500 = Fail +joblog_handleCode_502 = Timeout +joblog_kill_log = Kill Job +joblog_kill_log_limit = Trigger Fail, can not kill job +joblog_kill_log_byman = Manual operation, kill job +joblog_lost_fail = Job result lost, marked as failure +joblog_rolling_log = Rolling log +joblog_rolling_log_refresh = Refresh +joblog_rolling_log_triggerfail = The job trigger fail, can not view the rolling log +joblog_rolling_log_failoften = The request for the Rolling log is terminated, the number of failed requests exceeds the limit, Reload the log on the refresh page +joblog_logid_unvalid = Log ID is illegal + +## job group +jobgroup_name = Executor Manage +jobgroup_list = Executor List +jobgroup_add = Add Executor +jobgroup_edit = Edit Executor +jobgroup_del = Delete Executor +jobgroup_field_title = Title +jobgroup_field_addressType = Registry Type +jobgroup_field_addressType_0 = Automatic registration +jobgroup_field_addressType_1 = Manual registration +jobgroup_field_addressType_limit = Manually registration type, the machine address must not be empty +jobgroup_field_registryList = machine address +jobgroup_field_registryList_unvalid = registry machine address is illegal +jobgroup_field_registryList_placeholder = Please enter the machine address, if there are more than one comma separated +jobgroup_field_appname_limit = Limit the beginning of a lowercase letter, consists of lowercase letters、number and hyphen. +jobgroup_field_appname_length = AppName length is limited to 4~64 +jobgroup_field_title_length = Title length is limited to 4~12 +jobgroup_field_order_digits = Please enter a positive integer +jobgroup_field_orderrange = Order is limited to 1~1000 +jobgroup_del_limit_0 = Refuse to delete, the executor is being used +jobgroup_del_limit_1 = Refuses to delete, the system retains at least one executor +jobgroup_empty = There is no valid executor. Please contact the administrator + +## job conf +jobconf_block_SERIAL_EXECUTION = Serial execution +jobconf_block_DISCARD_LATER = Discard Later +jobconf_block_COVER_EARLY = Cover Early +jobconf_route_first = First +jobconf_route_last = Last +jobconf_route_round = Round +jobconf_route_random = Random +jobconf_route_consistenthash = Consistent Hash +jobconf_route_lfu = Least Frequently Used +jobconf_route_lru = Least Recently Used +jobconf_route_failover = Failover +jobconf_route_busyover = Busyover +jobconf_route_shard = Sharding Broadcast +jobconf_idleBeat = Idle check +jobconf_beat = Heartbeats +jobconf_monitor = Task Scheduling Center monitor alarm +jobconf_monitor_detail = monitor alarm details +jobconf_monitor_alarm_title = Alarm Type +jobconf_monitor_alarm_type = Trigger Fail +jobconf_monitor_alarm_content = Alarm Content +jobconf_trigger_admin_adress = Trigger machine address +jobconf_trigger_exe_regtype = Execotor-Registry Type +jobconf_trigger_exe_regaddress = Execotor-Registry Address +jobconf_trigger_address_empty = Trigger Fail:registry address is empty +jobconf_trigger_run = Trigger Job +jobconf_trigger_child_run = Trigger child job +jobconf_callback_child_msg1 = {0}/{1} [Job ID={2}], Trigger {3}, Trigger msg: {4}
+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 + +## help +job_help = Tutorial +job_help_document = Official Document diff --git a/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties new file mode 100644 index 0000000..dbe17b6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties @@ -0,0 +1,275 @@ +admin_name = 任务调度中心 +admin_name_full = 分布式任务调度平台XXL-JOB +admin_version = 2.4.1 +admin_i18n = +## system +system_tips = 系统提示 +system_ok = 确定 +system_close = 关闭 +system_save = 保存 +system_cancel = 取消 +system_search = 搜索 +system_status = 状态 +system_opt = 操作 +system_please_input = 请输入 +system_please_choose = 请选择 +system_success = 成功 +system_fail = 失败 +system_add_suc = 新增成功 +system_add_fail = 新增失败 +system_update_suc = 更新成功 +system_update_fail = 更新失败 +system_all = 全部 +system_api_error = 接口异常 +system_show = 查看 +system_empty = 无 +system_opt_suc = 操作成功 +system_opt_fail = 操作失败 +system_opt_edit = 编辑 +system_opt_del = 删除 +system_opt_copy = 复制 +system_unvalid = 非法 +system_not_found = 不存在 +system_nav = 导航 +system_digits = 整数 +system_lengh_limit = 长度限制 +system_permission_limit = 权限拦截 +system_welcome = 欢迎 + +## daterangepicker +daterangepicker_ranges_recent_hour = 最近一小时 +daterangepicker_ranges_today = 今日 +daterangepicker_ranges_yesterday = 昨日 +daterangepicker_ranges_this_month = 本月 +daterangepicker_ranges_last_month = 上个月 +daterangepicker_ranges_recent_week = 最近一周 +daterangepicker_ranges_recent_month = 最近一月 +daterangepicker_custom_name = 自定义 +daterangepicker_custom_starttime = 起始时间 +daterangepicker_custom_endtime = 结束时间 +daterangepicker_custom_daysofweek = 日,一,二,三,四,五,六 +daterangepicker_custom_monthnames = 一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing = 处理中... +dataTable_sLengthMenu = 每页 _MENU_ 条记录 +dataTable_sZeroRecords = 没有匹配结果 +dataTable_sInfo = 第 _PAGE_ 页 ( 总共 _PAGES_ 页,_TOTAL_ 条记录 ) +dataTable_sInfoEmpty = 无记录 +dataTable_sInfoFiltered = (由 _MAX_ 项结果过滤) +dataTable_sSearch = 搜索 +dataTable_sEmptyTable = 表中数据为空 +dataTable_sLoadingRecords = 载入中... +dataTable_sFirst = 首页 +dataTable_sPrevious = 上页 +dataTable_sNext = 下页 +dataTable_sLast = 末页 +dataTable_sSortAscending = : 以升序排列此列 +dataTable_sSortDescending = : 以降序排列此列 + +## login +login_btn = 登录 +login_remember_me = 记住密码 +login_username_placeholder = 请输入登录账号 +login_password_placeholder = 请输入登录密码 +login_username_empty = 请输入登录账号 +login_username_lt_4 = 登录账号不应低于4位 +login_password_empty = 请输入登录密码 +login_password_lt_4 = 登录密码不应低于4位 +login_success = 登录成功 +login_fail = 登录失败 +login_param_empty = 账号或密码为空 +login_param_unvalid = 账号或密码错误 + +## logout +logout_btn = 注销 +logout_confirm = 确认注销登录? +logout_success = 注销成功 +logout_fail = 注销失败 + +## change pwd +change_pwd = 修改密码 +change_pwd_suc_to_logout = 修改密码成功,即将注销登陆 +change_pwd_field_newpwd = 新密码 + +## dashboard +job_dashboard_name = 运行报表 +job_dashboard_job_num = 任务数量 +job_dashboard_job_num_tip = 调度中心运行的任务数量 +job_dashboard_trigger_num = 调度次数 +job_dashboard_trigger_num_tip = 调度中心触发的调度次数 +job_dashboard_jobgroup_num = 执行器数量 +job_dashboard_jobgroup_num_tip = 调度中心在线的执行器机器数量 +job_dashboard_report = 调度报表 +job_dashboard_report_loaddata_fail = 调度报表数据加载异常 +job_dashboard_date_report = 日期分布图 +job_dashboard_rate_report = 成功比例图 + +## job info +jobinfo_name = 任务管理 +jobinfo_job = 任务 +jobinfo_field_add = 新增 +jobinfo_field_update = 更新任务 +jobinfo_field_id = 任务ID +jobinfo_field_jobgroup = 执行器 +jobinfo_field_jobdesc = 任务描述 +jobinfo_field_gluetype = 运行模式 +jobinfo_field_executorparam = 任务参数 +jobinfo_field_author = 负责人 +jobinfo_field_timeout = 任务超时时间 +jobinfo_field_alarmemail = 报警邮件 +jobinfo_field_alarmemail_placeholder = 请输入报警邮件,多个邮件地址则逗号分隔 +jobinfo_field_executorRouteStrategy = 路由策略 +jobinfo_field_childJobId = 子任务ID +jobinfo_field_childJobId_placeholder = 请输入子任务的任务ID,如存在多个则逗号分隔 +jobinfo_field_executorBlockStrategy = 阻塞处理策略 +jobinfo_field_executorFailRetryCount = 失败重试次数 +jobinfo_field_executorFailRetryCount_placeholder = 失败重试次数,大于零时生效 +jobinfo_script_location = 脚本位置 +jobinfo_shard_index = 分片序号 +jobinfo_shard_total = 分片总数 +jobinfo_opt_stop = 停止 +jobinfo_opt_start = 启动 +jobinfo_opt_log = 查询日志 +jobinfo_opt_run = 执行一次 +jobinfo_opt_run_tips = 请输入本次执行的机器地址,为空则从执行器获取 +jobinfo_opt_registryinfo = 注册节点 +jobinfo_opt_next_time = 下次执行时间 +jobinfo_glue_remark = 源码备注 +jobinfo_glue_remark_limit = 源码备注长度限制为4~100 +jobinfo_glue_rollback = 版本回溯 +jobinfo_glue_jobid_unvalid = 任务ID非法 +jobinfo_glue_gluetype_unvalid = 该任务非GLUE模式 +jobinfo_field_executorTimeout_placeholder = 任务超时时间,单位秒,大于零时生效 +schedule_type = 调度类型 +schedule_type_none = 无 +schedule_type_cron = CRON +schedule_type_fix_rate = 固定速度 +schedule_type_fix_delay = 固定延迟 +schedule_type_none_limit_start = 当前调度类型禁止启动 +misfire_strategy = 调度过期策略 +misfire_strategy_do_nothing = 忽略 +misfire_strategy_fire_once_now = 立即执行一次 +jobinfo_conf_base = 基础配置 +jobinfo_conf_schedule = 调度配置 +jobinfo_conf_job = 任务配置 +jobinfo_conf_advanced = 高级配置 + +## job log +joblog_name = 调度日志 +joblog_status = 状态 +joblog_status_all = 全部 +joblog_status_suc = 成功 +joblog_status_fail = 失败 +joblog_status_running = 进行中 +joblog_field_triggerTime = 调度时间 +joblog_field_triggerCode = 调度结果 +joblog_field_triggerMsg = 调度备注 +joblog_field_handleTime = 执行时间 +joblog_field_handleCode = 执行结果 +joblog_field_handleMsg = 执行备注 +joblog_field_executorAddress = 执行器地址 +joblog_clean = 清理 +joblog_clean_log = 日志清理 +joblog_clean_type = 清理方式 +joblog_clean_type_1 = 清理一个月之前日志数据 +joblog_clean_type_2 = 清理三个月之前日志数据 +joblog_clean_type_3 = 清理六个月之前日志数据 +joblog_clean_type_4 = 清理一年之前日志数据 +joblog_clean_type_5 = 清理一千条以前日志数据 +joblog_clean_type_6 = 清理一万条以前日志数据 +joblog_clean_type_7 = 清理三万条以前日志数据 +joblog_clean_type_8 = 清理十万条以前日志数据 +joblog_clean_type_9 = 清理所有日志数据 +joblog_clean_type_unvalid = 清理类型参数异常 +joblog_handleCode_200 = 成功 +joblog_handleCode_500 = 失败 +joblog_handleCode_502 = 失败(超时) +joblog_kill_log = 终止任务 +joblog_kill_log_limit = 调度失败,无法终止日志 +joblog_kill_log_byman = 人为操作,主动终止 +joblog_lost_fail = 任务结果丢失,标记失败 +joblog_rolling_log = 执行日志 +joblog_rolling_log_refresh = 刷新 +joblog_rolling_log_triggerfail = 任务发起调度失败,无法查看执行日志 +joblog_rolling_log_failoften = 终止请求Rolling日志,请求失败次数超上限,可刷新页面重新加载日志 +joblog_logid_unvalid = 日志ID非法 + +## job group +jobgroup_name = 执行器管理 +jobgroup_list = 执行器列表 +jobgroup_add = 新增执行器 +jobgroup_edit = 编辑执行器 +jobgroup_del = 删除执行器 +jobgroup_field_title = 名称 +jobgroup_field_addressType = 注册方式 +jobgroup_field_addressType_0 = 自动注册 +jobgroup_field_addressType_1 = 手动录入 +jobgroup_field_addressType_limit = 手动录入注册方式,机器地址不可为空 +jobgroup_field_registryList = 机器地址 +jobgroup_field_registryList_unvalid = 机器地址格式非法 +jobgroup_field_registryList_placeholder = 请输入执行器地址列表,多地址逗号分隔 +jobgroup_field_appname_limit = 限制以小写字母开头,由小写字母、数字和中划线组成 +jobgroup_field_appname_length = AppName长度限制为4~64 +jobgroup_field_title_length = 名称长度限制为4~12 +jobgroup_field_order_digits = 请输入整数 +jobgroup_field_orderrange = 取值范围为1~1000 +jobgroup_del_limit_0 = 拒绝删除,该执行器使用中 +jobgroup_del_limit_1 = 拒绝删除, 系统至少保留一个执行器 +jobgroup_empty = 不存在有效执行器,请联系管理员 + +## job conf +jobconf_block_SERIAL_EXECUTION = 单机串行 +jobconf_block_DISCARD_LATER = 丢弃后续调度 +jobconf_block_COVER_EARLY = 覆盖之前调度 +jobconf_route_first = 第一个 +jobconf_route_last = 最后一个 +jobconf_route_round = 轮询 +jobconf_route_random = 随机 +jobconf_route_consistenthash = 一致性HASH +jobconf_route_lfu = 最不经常使用 +jobconf_route_lru = 最近最久未使用 +jobconf_route_failover = 故障转移 +jobconf_route_busyover = 忙碌转移 +jobconf_route_shard = 分片广播 +jobconf_idleBeat = 空闲检测 +jobconf_beat = 心跳检测 +jobconf_monitor = 任务调度中心监控报警 +jobconf_monitor_detail = 监控告警明细 +jobconf_monitor_alarm_title = 告警类型 +jobconf_monitor_alarm_type = 调度失败 +jobconf_monitor_alarm_content = 告警内容 +jobconf_trigger_admin_adress = 调度机器 +jobconf_trigger_exe_regtype = 执行器-注册方式 +jobconf_trigger_exe_regaddress = 执行器-地址列表 +jobconf_trigger_address_empty = 调度失败:执行器地址为空 +jobconf_trigger_run = 触发调度 +jobconf_trigger_child_run = 触发子任务 +jobconf_callback_child_msg1 = {0}/{1} [任务ID={2}], 触发{3}, 触发备注: {4}
+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 = 禁止操作当前登录账号 + +## help +job_help = 使用教程 +job_help_document = 官方文档 diff --git a/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties new file mode 100644 index 0000000..8bb9f9e --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties @@ -0,0 +1,275 @@ +admin_name = 任務調度中心 +admin_name_full = 分布式任務調度平臺XXL-JOB +admin_version = 2.4.1 +admin_i18n = +## system +system_tips = 系統提示 +system_ok = 確定 +system_close = 關閉 +system_save = 儲存 +system_cancel = 取消 +system_search = 搜尋 +system_status = 狀態 +system_opt = 操作 +system_please_input = 請輸入 +system_please_choose = 请選擇 +system_success = 成功 +system_fail = 失敗 +system_add_suc = 新增成功 +system_add_fail = 新增失敗 +system_update_suc = 更新成功 +system_update_fail = 更新失敗 +system_all = 全部 +system_api_error = API錯誤 +system_show = 查看 +system_empty = 無 +system_opt_suc = 操作成功 +system_opt_fail = 操作失敗 +system_opt_edit = 編輯 +system_opt_del = 刪除 +system_opt_copy = 復制 +system_unvalid = 非法 +system_not_found = 不存在 +system_nav = 導航 +system_digits = 整數 +system_lengh_limit = 長度限制 +system_permission_limit = 權限控管 +system_welcome = 歡迎 + +## daterangepicker +daterangepicker_ranges_recent_hour = 最近一小時 +daterangepicker_ranges_today = 今日 +daterangepicker_ranges_yesterday = 昨日 +daterangepicker_ranges_this_month = 本月 +daterangepicker_ranges_last_month = 上個月 +daterangepicker_ranges_recent_week = 最近一周 +daterangepicker_ranges_recent_month = 最近一月 +daterangepicker_custom_name = 自定義 +daterangepicker_custom_starttime = 起始時間 +daterangepicker_custom_endtime = 結束時間 +daterangepicker_custom_daysofweek = 日,一,二,三,四,五,六 +daterangepicker_custom_monthnames = 一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing = 處理中... +dataTable_sLengthMenu = 每頁 _MENU_ 條記錄 +dataTable_sZeroRecords = 沒有相符合記錄 +dataTable_sInfo = 第 _PAGE_ 頁 ( 總共 _PAGES_ 頁,_TOTAL_ 條記錄 ) +dataTable_sInfoEmpty = 無記錄 +dataTable_sInfoFiltered = (由 _MAX_ 項結果過濾) +dataTable_sSearch = 搜尋 +dataTable_sEmptyTable = 表中資料為空 +dataTable_sLoadingRecords = 載入中... +dataTable_sFirst = 首頁 +dataTable_sPrevious = 上頁 +dataTable_sNext = 下頁 +dataTable_sLast = 末頁 +dataTable_sSortAscending = : 以升幂排序此列 +dataTable_sSortDescending = : 以降幂排序此列 + +## login +login_btn = 登入 +login_remember_me = 記住密碼 +login_username_placeholder = 請輸入登入帳號 +login_password_placeholder = 請輸入登入密碼 +login_username_empty = 請輸入登入帳號 +login_username_lt_4 = 登入帳號不應低於4位數 +login_password_empty = 請輸入登入密碼 +login_password_lt_4 = 登入密碼不應低於4位數 +login_success = 登入成功 +login_fail = 登入失敗 +login_param_empty = 帳號或密碼為空值 +login_param_unvalid = 帳號或密碼錯誤 + +## logout +logout_btn = 登出 +logout_confirm = 確認登出? +logout_success = 登出成功 +logout_fail = 登出失敗 + +## change pwd +change_pwd = 修改密碼 +change_pwd_suc_to_logout = 修改密碼成功,即將登出 +change_pwd_field_newpwd = 新密碼 + +## dashboard +job_dashboard_name = 運行報表 +job_dashboard_job_num = 任務數量 +job_dashboard_job_num_tip = 調度中心運行的任務數量 +job_dashboard_trigger_num = 調度次數 +job_dashboard_trigger_num_tip = 調度中心觸發的調度次數 +job_dashboard_jobgroup_num = 執行器數量 +job_dashboard_jobgroup_num_tip = 調度中心在線的執行器機器數量 +job_dashboard_report = 調度報表 +job_dashboard_report_loaddata_fail = 調度報表資料加載異常 +job_dashboard_date_report = 日期分布圖 +job_dashboard_rate_report = 成功比例圖 + +## job info +jobinfo_name = 任務管理 +jobinfo_job = 任務 +jobinfo_field_add = 新增 +jobinfo_field_update = 更新任務 +jobinfo_field_id = 任務ID +jobinfo_field_jobgroup = 執行器 +jobinfo_field_jobdesc = 任務描述 +jobinfo_field_gluetype = 運行模式 +jobinfo_field_executorparam = 任務參數 +jobinfo_field_author = 負責人 +jobinfo_field_timeout = 任務超時秒數 +jobinfo_field_alarmemail = 告警郵件 +jobinfo_field_alarmemail_placeholder = 輸入多個告警郵件地址,請以逗號分隔 +jobinfo_field_executorRouteStrategy = 路由策略 +jobinfo_field_childJobId = 子任務ID +jobinfo_field_childJobId_placeholder = 輸入子任務ID,如有多個請以逗號分隔 +jobinfo_field_executorBlockStrategy = 阻塞處理策略 +jobinfo_field_executorFailRetryCount = 失敗重試次數 +jobinfo_field_executorFailRetryCount_placeholder = 失敗重試次數,大於零時生效 +jobinfo_script_location = 腳本位置 +jobinfo_shard_index = 分片序號 +jobinfo_shard_total = 分片總數 +jobinfo_opt_stop = 停止 +jobinfo_opt_start = 啟動 +jobinfo_opt_log = 查詢日誌 +jobinfo_opt_run = 執行一次 +jobinfo_opt_run_tips = 請輸入本次執行的機器地址,為空則從執行器獲取 +jobinfo_opt_registryinfo = 注冊節點 +jobinfo_opt_next_time = 下次執行時間 +jobinfo_glue_remark = 源碼備註 +jobinfo_glue_remark_limit = 源碼備註長度限制為4~100 +jobinfo_glue_rollback = 版本回復 +jobinfo_glue_jobid_unvalid = 任務ID非法 +jobinfo_glue_gluetype_unvalid = 該任務非GLUE模式 +jobinfo_field_executorTimeout_placeholder = 任務超時時間,單位秒,大於零時生效 +schedule_type = 調度類型 +schedule_type_none = 無 +schedule_type_cron = CRON +schedule_type_fix_rate = 固定速度 +schedule_type_fix_delay = 固定延遲 +schedule_type_none_limit_start = 當前調度類型禁止啟動 +misfire_strategy = 調度過期策略 +misfire_strategy_do_nothing = 忽略 +misfire_strategy_fire_once_now = 立即執行壹次 +jobinfo_conf_base = 基礎配置 +jobinfo_conf_schedule = 調度配置 +jobinfo_conf_job = 任務配置 +jobinfo_conf_advanced = 高級配置 + +## job log +joblog_name = 調度日誌 +joblog_status = 狀態 +joblog_status_all = 全部 +joblog_status_suc = 成功 +joblog_status_fail = 失敗 +joblog_status_running = 進行中 +joblog_field_triggerTime = 調度時間 +joblog_field_triggerCode = 調度結果 +joblog_field_triggerMsg = 調度備註 +joblog_field_handleTime = 執行時間 +joblog_field_handleCode = 執行结果 +joblog_field_handleMsg = 執行備註 +joblog_field_executorAddress = 執行器地址 +joblog_clean = 清理 +joblog_clean_log = 日誌清理 +joblog_clean_type = 清理方式 +joblog_clean_type_1 = 清理一個月之前日誌資料 +joblog_clean_type_2 = 清理三個月之前日誌資料 +joblog_clean_type_3 = 清理六個月之前日誌資料 +joblog_clean_type_4 = 清理一年之前日誌資料 +joblog_clean_type_5 = 清理一千條以前日誌資料 +joblog_clean_type_6 = 清理一萬條以前日誌資料 +joblog_clean_type_7 = 清理三萬條以前日誌資料 +joblog_clean_type_8 = 清理十萬條以前日誌資料 +joblog_clean_type_9 = 清理所有日誌資料 +joblog_clean_type_unvalid = 清理類型參数異常 +joblog_handleCode_200 = 成功 +joblog_handleCode_500 = 失敗 +joblog_handleCode_502 = 失敗(超時) +joblog_kill_log = 终止任務 +joblog_kill_log_limit = 調度失敗,無法终止日誌 +joblog_kill_log_byman = 人為操作,主動終止 +joblog_lost_fail = 任務結果丟失,標記失敗 +joblog_rolling_log = 執行日誌 +joblog_rolling_log_refresh = 更新 +joblog_rolling_log_triggerfail = 任務發起調度失敗,無法查看執行日誌 +joblog_rolling_log_failoften = 終止請求Rolling日誌,請求失敗次數超上限,可刷新頁面重新加載日誌 +joblog_logid_unvalid = 日誌ID非法 + +## job group +jobgroup_name = 執行器管理 +jobgroup_list = 執行器列表 +jobgroup_add = 新增執行器 +jobgroup_edit = 編輯執行器 +jobgroup_del = 刪除執行器 +jobgroup_field_title = 名稱 +jobgroup_field_addressType = 注冊方式 +jobgroup_field_addressType_0 = 自動注冊 +jobgroup_field_addressType_1 = 手動登錄 +jobgroup_field_addressType_limit = 手動登錄注冊方式,機器地址不可為空 +jobgroup_field_registryList = 機器地址 +jobgroup_field_registryList_unvalid = 機器地址格式非法 +jobgroup_field_registryList_placeholder = 請輸入執行器地址列表,多個地址請以逗號分隔 +jobgroup_field_appname_limit = 限制以小寫字母開頭,由小寫字母、數字和中划線組成 +jobgroup_field_appname_length = AppName長度限制為4~64 +jobgroup_field_title_length = 名稱長度限制為4~12 +jobgroup_field_order_digits = 請輸入整數 +jobgroup_field_orderrange = 取值範圍為1~1000 +jobgroup_del_limit_0 = 拒絕刪除,該執行器使用中 +jobgroup_del_limit_1 = 拒絕删除,系统至少保留一個執行器 +jobgroup_empty = 不存在有效執行器,請聯絡系統管理員 + +## job conf +jobconf_block_SERIAL_EXECUTION = 單機串行 +jobconf_block_DISCARD_LATER = 丢棄后續調度 +jobconf_block_COVER_EARLY = 覆蓋之前調度 +jobconf_route_first = 第一個 +jobconf_route_last = 最後一個 +jobconf_route_round = 輪詢 +jobconf_route_random = 隨機 +jobconf_route_consistenthash = 一致性HASH +jobconf_route_lfu = 最不經常使用 +jobconf_route_lru = 最近最久未使用 +jobconf_route_failover = 故障轉移 +jobconf_route_busyover = 忙碌轉移 +jobconf_route_shard = 分片廣播 +jobconf_idleBeat = 空閒檢測 +jobconf_beat = 心跳檢測 +jobconf_monitor = 任務調度中心監控告警 +jobconf_monitor_detail = 監控告警明细 +jobconf_monitor_alarm_title = 告警類型 +jobconf_monitor_alarm_type = 調度失敗 +jobconf_monitor_alarm_content = 告警内容 +jobconf_trigger_admin_adress = 調度機器 +jobconf_trigger_exe_regtype = 執行器-注冊方式 +jobconf_trigger_exe_regaddress = 執行器-地址列表 +jobconf_trigger_address_empty = 調度失敗:執行器地址為空 +jobconf_trigger_run = 觸發調度 +jobconf_trigger_child_run = 觸發子任務 +jobconf_callback_child_msg1 = {0}/{1} [任務ID={2}], 觸發{3}, 觸發備註: {4}
+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 = 禁止操作當前登入帳號 + +## help +job_help = 使用教程 +job_help_document = 官方文件 diff --git a/xxl-job/xxl-job-admin/src/main/resources/logback-spring.xml b/xxl-job/xxl-job-admin/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..8f7c364 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/logback-spring.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + false + + ${console_pattern} + + + + + ${log_path}/${service_name}/${service_name}.log + + ${log_path}/${service_name}/%d{yyyy-MM, aux}/${service_name}.%d{yyyy-MM-dd}.%i.log.zip + 50MB + 30 + + + ${file_pattern} + + + + + + + + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml new file mode 100644 index 0000000..06418c6 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + 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} + + + + + + + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml new file mode 100644 index 0000000..e8848c7 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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} + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml new file mode 100644 index 0000000..756eebf --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + 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} + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml new file mode 100644 index 0000000..b0d4dfe --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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} + + + + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml new file mode 100644 index 0000000..abff18c --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + 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} + ); + + + + + UPDATE xxl_job_log_report + SET `running_count` = #{runningCount}, + `suc_count` = #{sucCount}, + `fail_count` = #{failCount} + WHERE `trigger_day` = #{triggerDay} + + + + + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml new file mode 100644 index 0000000..4175849 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + t.id, + t.registry_group, + t.registry_key, + t.registry_value, + t.update_time + + + + + + DELETE FROM xxl_job_registry + WHERE id in + + #{item} + + + + + + + UPDATE xxl_job_registry + SET `update_time` = #{updateTime} + WHERE `registry_group` = #{registryGroup} + AND `registry_key` = #{registryKey} + AND `registry_value` = #{registryValue} + + + + INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`) + VALUES( #{registryGroup} , #{registryKey} , #{registryValue}, #{updateTime}) + + + + DELETE FROM xxl_job_registry + WHERE registry_group = #{registryGroup} + AND registry_key = #{registryKey} + AND registry_value = #{registryValue} + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml new file mode 100644 index 0000000..105ec2f --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + t.id, + t.username, + t.password, + 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} + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css new file mode 100644 index 0000000..baba9e9 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css @@ -0,0 +1,11 @@ +@charset "UTF-8";/*! + Ionicons, v2.0.0 + Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ + https://twitter.com/benjsperry https://twitter.com/ionicframework + MIT License: https://github.com/driftyco/ionicons + + Android-style icons originally built by Google’s + Material Design Icons: https://github.com/google/material-design-icons + used under CC BY http://creativecommons.org/licenses/by/4.0/ + Modified icons to fit ionicon’s grid from original. +*/@font-face{font-family:"Ionicons";src:url("../fonts/ionicons.eot?v=2.0.0");src:url("../fonts/ionicons.eot?v=2.0.0#iefix") format("embedded-opentype"),url("../fonts/ionicons.ttf?v=2.0.0") format("truetype"),url("../fonts/ionicons.woff?v=2.0.0") format("woff"),url("../fonts/ionicons.svg?v=2.0.0#Ionicons") format("svg");font-weight:normal;font-style:normal}.ion,.ionicons,.ion-alert:before,.ion-alert-circled:before,.ion-android-add:before,.ion-android-add-circle:before,.ion-android-alarm-clock:before,.ion-android-alert:before,.ion-android-apps:before,.ion-android-archive:before,.ion-android-arrow-back:before,.ion-android-arrow-down:before,.ion-android-arrow-dropdown:before,.ion-android-arrow-dropdown-circle:before,.ion-android-arrow-dropleft:before,.ion-android-arrow-dropleft-circle:before,.ion-android-arrow-dropright:before,.ion-android-arrow-dropright-circle:before,.ion-android-arrow-dropup:before,.ion-android-arrow-dropup-circle:before,.ion-android-arrow-forward:before,.ion-android-arrow-up:before,.ion-android-attach:before,.ion-android-bar:before,.ion-android-bicycle:before,.ion-android-boat:before,.ion-android-bookmark:before,.ion-android-bulb:before,.ion-android-bus:before,.ion-android-calendar:before,.ion-android-call:before,.ion-android-camera:before,.ion-android-cancel:before,.ion-android-car:before,.ion-android-cart:before,.ion-android-chat:before,.ion-android-checkbox:before,.ion-android-checkbox-blank:before,.ion-android-checkbox-outline:before,.ion-android-checkbox-outline-blank:before,.ion-android-checkmark-circle:before,.ion-android-clipboard:before,.ion-android-close:before,.ion-android-cloud:before,.ion-android-cloud-circle:before,.ion-android-cloud-done:before,.ion-android-cloud-outline:before,.ion-android-color-palette:before,.ion-android-compass:before,.ion-android-contact:before,.ion-android-contacts:before,.ion-android-contract:before,.ion-android-create:before,.ion-android-delete:before,.ion-android-desktop:before,.ion-android-document:before,.ion-android-done:before,.ion-android-done-all:before,.ion-android-download:before,.ion-android-drafts:before,.ion-android-exit:before,.ion-android-expand:before,.ion-android-favorite:before,.ion-android-favorite-outline:before,.ion-android-film:before,.ion-android-folder:before,.ion-android-folder-open:before,.ion-android-funnel:before,.ion-android-globe:before,.ion-android-hand:before,.ion-android-hangout:before,.ion-android-happy:before,.ion-android-home:before,.ion-android-image:before,.ion-android-laptop:before,.ion-android-list:before,.ion-android-locate:before,.ion-android-lock:before,.ion-android-mail:before,.ion-android-map:before,.ion-android-menu:before,.ion-android-microphone:before,.ion-android-microphone-off:before,.ion-android-more-horizontal:before,.ion-android-more-vertical:before,.ion-android-navigate:before,.ion-android-notifications:before,.ion-android-notifications-none:before,.ion-android-notifications-off:before,.ion-android-open:before,.ion-android-options:before,.ion-android-people:before,.ion-android-person:before,.ion-android-person-add:before,.ion-android-phone-landscape:before,.ion-android-phone-portrait:before,.ion-android-pin:before,.ion-android-plane:before,.ion-android-playstore:before,.ion-android-print:before,.ion-android-radio-button-off:before,.ion-android-radio-button-on:before,.ion-android-refresh:before,.ion-android-remove:before,.ion-android-remove-circle:before,.ion-android-restaurant:before,.ion-android-sad:before,.ion-android-search:before,.ion-android-send:before,.ion-android-settings:before,.ion-android-share:before,.ion-android-share-alt:before,.ion-android-star:before,.ion-android-star-half:before,.ion-android-star-outline:before,.ion-android-stopwatch:before,.ion-android-subway:before,.ion-android-sunny:before,.ion-android-sync:before,.ion-android-textsms:before,.ion-android-time:before,.ion-android-train:before,.ion-android-unlock:before,.ion-android-upload:before,.ion-android-volume-down:before,.ion-android-volume-mute:before,.ion-android-volume-off:before,.ion-android-volume-up:before,.ion-android-walk:before,.ion-android-warning:before,.ion-android-watch:before,.ion-android-wifi:before,.ion-aperture:before,.ion-archive:before,.ion-arrow-down-a:before,.ion-arrow-down-b:before,.ion-arrow-down-c:before,.ion-arrow-expand:before,.ion-arrow-graph-down-left:before,.ion-arrow-graph-down-right:before,.ion-arrow-graph-up-left:before,.ion-arrow-graph-up-right:before,.ion-arrow-left-a:before,.ion-arrow-left-b:before,.ion-arrow-left-c:before,.ion-arrow-move:before,.ion-arrow-resize:before,.ion-arrow-return-left:before,.ion-arrow-return-right:before,.ion-arrow-right-a:before,.ion-arrow-right-b:before,.ion-arrow-right-c:before,.ion-arrow-shrink:before,.ion-arrow-swap:before,.ion-arrow-up-a:before,.ion-arrow-up-b:before,.ion-arrow-up-c:before,.ion-asterisk:before,.ion-at:before,.ion-backspace:before,.ion-backspace-outline:before,.ion-bag:before,.ion-battery-charging:before,.ion-battery-empty:before,.ion-battery-full:before,.ion-battery-half:before,.ion-battery-low:before,.ion-beaker:before,.ion-beer:before,.ion-bluetooth:before,.ion-bonfire:before,.ion-bookmark:before,.ion-bowtie:before,.ion-briefcase:before,.ion-bug:before,.ion-calculator:before,.ion-calendar:before,.ion-camera:before,.ion-card:before,.ion-cash:before,.ion-chatbox:before,.ion-chatbox-working:before,.ion-chatboxes:before,.ion-chatbubble:before,.ion-chatbubble-working:before,.ion-chatbubbles:before,.ion-checkmark:before,.ion-checkmark-circled:before,.ion-checkmark-round:before,.ion-chevron-down:before,.ion-chevron-left:before,.ion-chevron-right:before,.ion-chevron-up:before,.ion-clipboard:before,.ion-clock:before,.ion-close:before,.ion-close-circled:before,.ion-close-round:before,.ion-closed-captioning:before,.ion-cloud:before,.ion-code:before,.ion-code-download:before,.ion-code-working:before,.ion-coffee:before,.ion-compass:before,.ion-compose:before,.ion-connection-bars:before,.ion-contrast:before,.ion-crop:before,.ion-cube:before,.ion-disc:before,.ion-document:before,.ion-document-text:before,.ion-drag:before,.ion-earth:before,.ion-easel:before,.ion-edit:before,.ion-egg:before,.ion-eject:before,.ion-email:before,.ion-email-unread:before,.ion-erlenmeyer-flask:before,.ion-erlenmeyer-flask-bubbles:before,.ion-eye:before,.ion-eye-disabled:before,.ion-female:before,.ion-filing:before,.ion-film-marker:before,.ion-fireball:before,.ion-flag:before,.ion-flame:before,.ion-flash:before,.ion-flash-off:before,.ion-folder:before,.ion-fork:before,.ion-fork-repo:before,.ion-forward:before,.ion-funnel:before,.ion-gear-a:before,.ion-gear-b:before,.ion-grid:before,.ion-hammer:before,.ion-happy:before,.ion-happy-outline:before,.ion-headphone:before,.ion-heart:before,.ion-heart-broken:before,.ion-help:before,.ion-help-buoy:before,.ion-help-circled:before,.ion-home:before,.ion-icecream:before,.ion-image:before,.ion-images:before,.ion-information:before,.ion-information-circled:before,.ion-ionic:before,.ion-ios-alarm:before,.ion-ios-alarm-outline:before,.ion-ios-albums:before,.ion-ios-albums-outline:before,.ion-ios-americanfootball:before,.ion-ios-americanfootball-outline:before,.ion-ios-analytics:before,.ion-ios-analytics-outline:before,.ion-ios-arrow-back:before,.ion-ios-arrow-down:before,.ion-ios-arrow-forward:before,.ion-ios-arrow-left:before,.ion-ios-arrow-right:before,.ion-ios-arrow-thin-down:before,.ion-ios-arrow-thin-left:before,.ion-ios-arrow-thin-right:before,.ion-ios-arrow-thin-up:before,.ion-ios-arrow-up:before,.ion-ios-at:before,.ion-ios-at-outline:before,.ion-ios-barcode:before,.ion-ios-barcode-outline:before,.ion-ios-baseball:before,.ion-ios-baseball-outline:before,.ion-ios-basketball:before,.ion-ios-basketball-outline:before,.ion-ios-bell:before,.ion-ios-bell-outline:before,.ion-ios-body:before,.ion-ios-body-outline:before,.ion-ios-bolt:before,.ion-ios-bolt-outline:before,.ion-ios-book:before,.ion-ios-book-outline:before,.ion-ios-bookmarks:before,.ion-ios-bookmarks-outline:before,.ion-ios-box:before,.ion-ios-box-outline:before,.ion-ios-briefcase:before,.ion-ios-briefcase-outline:before,.ion-ios-browsers:before,.ion-ios-browsers-outline:before,.ion-ios-calculator:before,.ion-ios-calculator-outline:before,.ion-ios-calendar:before,.ion-ios-calendar-outline:before,.ion-ios-camera:before,.ion-ios-camera-outline:before,.ion-ios-cart:before,.ion-ios-cart-outline:before,.ion-ios-chatboxes:before,.ion-ios-chatboxes-outline:before,.ion-ios-chatbubble:before,.ion-ios-chatbubble-outline:before,.ion-ios-checkmark:before,.ion-ios-checkmark-empty:before,.ion-ios-checkmark-outline:before,.ion-ios-circle-filled:before,.ion-ios-circle-outline:before,.ion-ios-clock:before,.ion-ios-clock-outline:before,.ion-ios-close:before,.ion-ios-close-empty:before,.ion-ios-close-outline:before,.ion-ios-cloud:before,.ion-ios-cloud-download:before,.ion-ios-cloud-download-outline:before,.ion-ios-cloud-outline:before,.ion-ios-cloud-upload:before,.ion-ios-cloud-upload-outline:before,.ion-ios-cloudy:before,.ion-ios-cloudy-night:before,.ion-ios-cloudy-night-outline:before,.ion-ios-cloudy-outline:before,.ion-ios-cog:before,.ion-ios-cog-outline:before,.ion-ios-color-filter:before,.ion-ios-color-filter-outline:before,.ion-ios-color-wand:before,.ion-ios-color-wand-outline:before,.ion-ios-compose:before,.ion-ios-compose-outline:before,.ion-ios-contact:before,.ion-ios-contact-outline:before,.ion-ios-copy:before,.ion-ios-copy-outline:before,.ion-ios-crop:before,.ion-ios-crop-strong:before,.ion-ios-download:before,.ion-ios-download-outline:before,.ion-ios-drag:before,.ion-ios-email:before,.ion-ios-email-outline:before,.ion-ios-eye:before,.ion-ios-eye-outline:before,.ion-ios-fastforward:before,.ion-ios-fastforward-outline:before,.ion-ios-filing:before,.ion-ios-filing-outline:before,.ion-ios-film:before,.ion-ios-film-outline:before,.ion-ios-flag:before,.ion-ios-flag-outline:before,.ion-ios-flame:before,.ion-ios-flame-outline:before,.ion-ios-flask:before,.ion-ios-flask-outline:before,.ion-ios-flower:before,.ion-ios-flower-outline:before,.ion-ios-folder:before,.ion-ios-folder-outline:before,.ion-ios-football:before,.ion-ios-football-outline:before,.ion-ios-game-controller-a:before,.ion-ios-game-controller-a-outline:before,.ion-ios-game-controller-b:before,.ion-ios-game-controller-b-outline:before,.ion-ios-gear:before,.ion-ios-gear-outline:before,.ion-ios-glasses:before,.ion-ios-glasses-outline:before,.ion-ios-grid-view:before,.ion-ios-grid-view-outline:before,.ion-ios-heart:before,.ion-ios-heart-outline:before,.ion-ios-help:before,.ion-ios-help-empty:before,.ion-ios-help-outline:before,.ion-ios-home:before,.ion-ios-home-outline:before,.ion-ios-infinite:before,.ion-ios-infinite-outline:before,.ion-ios-information:before,.ion-ios-information-empty:before,.ion-ios-information-outline:before,.ion-ios-ionic-outline:before,.ion-ios-keypad:before,.ion-ios-keypad-outline:before,.ion-ios-lightbulb:before,.ion-ios-lightbulb-outline:before,.ion-ios-list:before,.ion-ios-list-outline:before,.ion-ios-location:before,.ion-ios-location-outline:before,.ion-ios-locked:before,.ion-ios-locked-outline:before,.ion-ios-loop:before,.ion-ios-loop-strong:before,.ion-ios-medical:before,.ion-ios-medical-outline:before,.ion-ios-medkit:before,.ion-ios-medkit-outline:before,.ion-ios-mic:before,.ion-ios-mic-off:before,.ion-ios-mic-outline:before,.ion-ios-minus:before,.ion-ios-minus-empty:before,.ion-ios-minus-outline:before,.ion-ios-monitor:before,.ion-ios-monitor-outline:before,.ion-ios-moon:before,.ion-ios-moon-outline:before,.ion-ios-more:before,.ion-ios-more-outline:before,.ion-ios-musical-note:before,.ion-ios-musical-notes:before,.ion-ios-navigate:before,.ion-ios-navigate-outline:before,.ion-ios-nutrition:before,.ion-ios-nutrition-outline:before,.ion-ios-paper:before,.ion-ios-paper-outline:before,.ion-ios-paperplane:before,.ion-ios-paperplane-outline:before,.ion-ios-partlysunny:before,.ion-ios-partlysunny-outline:before,.ion-ios-pause:before,.ion-ios-pause-outline:before,.ion-ios-paw:before,.ion-ios-paw-outline:before,.ion-ios-people:before,.ion-ios-people-outline:before,.ion-ios-person:before,.ion-ios-person-outline:before,.ion-ios-personadd:before,.ion-ios-personadd-outline:before,.ion-ios-photos:before,.ion-ios-photos-outline:before,.ion-ios-pie:before,.ion-ios-pie-outline:before,.ion-ios-pint:before,.ion-ios-pint-outline:before,.ion-ios-play:before,.ion-ios-play-outline:before,.ion-ios-plus:before,.ion-ios-plus-empty:before,.ion-ios-plus-outline:before,.ion-ios-pricetag:before,.ion-ios-pricetag-outline:before,.ion-ios-pricetags:before,.ion-ios-pricetags-outline:before,.ion-ios-printer:before,.ion-ios-printer-outline:before,.ion-ios-pulse:before,.ion-ios-pulse-strong:before,.ion-ios-rainy:before,.ion-ios-rainy-outline:before,.ion-ios-recording:before,.ion-ios-recording-outline:before,.ion-ios-redo:before,.ion-ios-redo-outline:before,.ion-ios-refresh:before,.ion-ios-refresh-empty:before,.ion-ios-refresh-outline:before,.ion-ios-reload:before,.ion-ios-reverse-camera:before,.ion-ios-reverse-camera-outline:before,.ion-ios-rewind:before,.ion-ios-rewind-outline:before,.ion-ios-rose:before,.ion-ios-rose-outline:before,.ion-ios-search:before,.ion-ios-search-strong:before,.ion-ios-settings:before,.ion-ios-settings-strong:before,.ion-ios-shuffle:before,.ion-ios-shuffle-strong:before,.ion-ios-skipbackward:before,.ion-ios-skipbackward-outline:before,.ion-ios-skipforward:before,.ion-ios-skipforward-outline:before,.ion-ios-snowy:before,.ion-ios-speedometer:before,.ion-ios-speedometer-outline:before,.ion-ios-star:before,.ion-ios-star-half:before,.ion-ios-star-outline:before,.ion-ios-stopwatch:before,.ion-ios-stopwatch-outline:before,.ion-ios-sunny:before,.ion-ios-sunny-outline:before,.ion-ios-telephone:before,.ion-ios-telephone-outline:before,.ion-ios-tennisball:before,.ion-ios-tennisball-outline:before,.ion-ios-thunderstorm:before,.ion-ios-thunderstorm-outline:before,.ion-ios-time:before,.ion-ios-time-outline:before,.ion-ios-timer:before,.ion-ios-timer-outline:before,.ion-ios-toggle:before,.ion-ios-toggle-outline:before,.ion-ios-trash:before,.ion-ios-trash-outline:before,.ion-ios-undo:before,.ion-ios-undo-outline:before,.ion-ios-unlocked:before,.ion-ios-unlocked-outline:before,.ion-ios-upload:before,.ion-ios-upload-outline:before,.ion-ios-videocam:before,.ion-ios-videocam-outline:before,.ion-ios-volume-high:before,.ion-ios-volume-low:before,.ion-ios-wineglass:before,.ion-ios-wineglass-outline:before,.ion-ios-world:before,.ion-ios-world-outline:before,.ion-ipad:before,.ion-iphone:before,.ion-ipod:before,.ion-jet:before,.ion-key:before,.ion-knife:before,.ion-laptop:before,.ion-leaf:before,.ion-levels:before,.ion-lightbulb:before,.ion-link:before,.ion-load-a:before,.ion-load-b:before,.ion-load-c:before,.ion-load-d:before,.ion-location:before,.ion-lock-combination:before,.ion-locked:before,.ion-log-in:before,.ion-log-out:before,.ion-loop:before,.ion-magnet:before,.ion-male:before,.ion-man:before,.ion-map:before,.ion-medkit:before,.ion-merge:before,.ion-mic-a:before,.ion-mic-b:before,.ion-mic-c:before,.ion-minus:before,.ion-minus-circled:before,.ion-minus-round:before,.ion-model-s:before,.ion-monitor:before,.ion-more:before,.ion-mouse:before,.ion-music-note:before,.ion-navicon:before,.ion-navicon-round:before,.ion-navigate:before,.ion-network:before,.ion-no-smoking:before,.ion-nuclear:before,.ion-outlet:before,.ion-paintbrush:before,.ion-paintbucket:before,.ion-paper-airplane:before,.ion-paperclip:before,.ion-pause:before,.ion-person:before,.ion-person-add:before,.ion-person-stalker:before,.ion-pie-graph:before,.ion-pin:before,.ion-pinpoint:before,.ion-pizza:before,.ion-plane:before,.ion-planet:before,.ion-play:before,.ion-playstation:before,.ion-plus:before,.ion-plus-circled:before,.ion-plus-round:before,.ion-podium:before,.ion-pound:before,.ion-power:before,.ion-pricetag:before,.ion-pricetags:before,.ion-printer:before,.ion-pull-request:before,.ion-qr-scanner:before,.ion-quote:before,.ion-radio-waves:before,.ion-record:before,.ion-refresh:before,.ion-reply:before,.ion-reply-all:before,.ion-ribbon-a:before,.ion-ribbon-b:before,.ion-sad:before,.ion-sad-outline:before,.ion-scissors:before,.ion-search:before,.ion-settings:before,.ion-share:before,.ion-shuffle:before,.ion-skip-backward:before,.ion-skip-forward:before,.ion-social-android:before,.ion-social-android-outline:before,.ion-social-angular:before,.ion-social-angular-outline:before,.ion-social-apple:before,.ion-social-apple-outline:before,.ion-social-bitcoin:before,.ion-social-bitcoin-outline:before,.ion-social-buffer:before,.ion-social-buffer-outline:before,.ion-social-chrome:before,.ion-social-chrome-outline:before,.ion-social-codepen:before,.ion-social-codepen-outline:before,.ion-social-css3:before,.ion-social-css3-outline:before,.ion-social-designernews:before,.ion-social-designernews-outline:before,.ion-social-dribbble:before,.ion-social-dribbble-outline:before,.ion-social-dropbox:before,.ion-social-dropbox-outline:before,.ion-social-euro:before,.ion-social-euro-outline:before,.ion-social-facebook:before,.ion-social-facebook-outline:before,.ion-social-foursquare:before,.ion-social-foursquare-outline:before,.ion-social-freebsd-devil:before,.ion-social-github:before,.ion-social-github-outline:before,.ion-social-google:before,.ion-social-google-outline:before,.ion-social-googleplus:before,.ion-social-googleplus-outline:before,.ion-social-hackernews:before,.ion-social-hackernews-outline:before,.ion-social-html5:before,.ion-social-html5-outline:before,.ion-social-instagram:before,.ion-social-instagram-outline:before,.ion-social-javascript:before,.ion-social-javascript-outline:before,.ion-social-linkedin:before,.ion-social-linkedin-outline:before,.ion-social-markdown:before,.ion-social-nodejs:before,.ion-social-octocat:before,.ion-social-pinterest:before,.ion-social-pinterest-outline:before,.ion-social-python:before,.ion-social-reddit:before,.ion-social-reddit-outline:before,.ion-social-rss:before,.ion-social-rss-outline:before,.ion-social-sass:before,.ion-social-skype:before,.ion-social-skype-outline:before,.ion-social-snapchat:before,.ion-social-snapchat-outline:before,.ion-social-tumblr:before,.ion-social-tumblr-outline:before,.ion-social-tux:before,.ion-social-twitch:before,.ion-social-twitch-outline:before,.ion-social-twitter:before,.ion-social-twitter-outline:before,.ion-social-usd:before,.ion-social-usd-outline:before,.ion-social-vimeo:before,.ion-social-vimeo-outline:before,.ion-social-whatsapp:before,.ion-social-whatsapp-outline:before,.ion-social-windows:before,.ion-social-windows-outline:before,.ion-social-wordpress:before,.ion-social-wordpress-outline:before,.ion-social-yahoo:before,.ion-social-yahoo-outline:before,.ion-social-yen:before,.ion-social-yen-outline:before,.ion-social-youtube:before,.ion-social-youtube-outline:before,.ion-soup-can:before,.ion-soup-can-outline:before,.ion-speakerphone:before,.ion-speedometer:before,.ion-spoon:before,.ion-star:before,.ion-stats-bars:before,.ion-steam:before,.ion-stop:before,.ion-thermometer:before,.ion-thumbsdown:before,.ion-thumbsup:before,.ion-toggle:before,.ion-toggle-filled:before,.ion-transgender:before,.ion-trash-a:before,.ion-trash-b:before,.ion-trophy:before,.ion-tshirt:before,.ion-tshirt-outline:before,.ion-umbrella:before,.ion-university:before,.ion-unlocked:before,.ion-upload:before,.ion-usb:before,.ion-videocamera:before,.ion-volume-high:before,.ion-volume-low:before,.ion-volume-medium:before,.ion-volume-mute:before,.ion-wand:before,.ion-waterdrop:before,.ion-wifi:before,.ion-wineglass:before,.ion-woman:before,.ion-wrench:before,.ion-xbox:before{display:inline-block;font-family:"Ionicons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ion-alert:before{content:"\f101"}.ion-alert-circled:before{content:"\f100"}.ion-android-add:before{content:"\f2c7"}.ion-android-add-circle:before{content:"\f359"}.ion-android-alarm-clock:before{content:"\f35a"}.ion-android-alert:before{content:"\f35b"}.ion-android-apps:before{content:"\f35c"}.ion-android-archive:before{content:"\f2c9"}.ion-android-arrow-back:before{content:"\f2ca"}.ion-android-arrow-down:before{content:"\f35d"}.ion-android-arrow-dropdown:before{content:"\f35f"}.ion-android-arrow-dropdown-circle:before{content:"\f35e"}.ion-android-arrow-dropleft:before{content:"\f361"}.ion-android-arrow-dropleft-circle:before{content:"\f360"}.ion-android-arrow-dropright:before{content:"\f363"}.ion-android-arrow-dropright-circle:before{content:"\f362"}.ion-android-arrow-dropup:before{content:"\f365"}.ion-android-arrow-dropup-circle:before{content:"\f364"}.ion-android-arrow-forward:before{content:"\f30f"}.ion-android-arrow-up:before{content:"\f366"}.ion-android-attach:before{content:"\f367"}.ion-android-bar:before{content:"\f368"}.ion-android-bicycle:before{content:"\f369"}.ion-android-boat:before{content:"\f36a"}.ion-android-bookmark:before{content:"\f36b"}.ion-android-bulb:before{content:"\f36c"}.ion-android-bus:before{content:"\f36d"}.ion-android-calendar:before{content:"\f2d1"}.ion-android-call:before{content:"\f2d2"}.ion-android-camera:before{content:"\f2d3"}.ion-android-cancel:before{content:"\f36e"}.ion-android-car:before{content:"\f36f"}.ion-android-cart:before{content:"\f370"}.ion-android-chat:before{content:"\f2d4"}.ion-android-checkbox:before{content:"\f374"}.ion-android-checkbox-blank:before{content:"\f371"}.ion-android-checkbox-outline:before{content:"\f373"}.ion-android-checkbox-outline-blank:before{content:"\f372"}.ion-android-checkmark-circle:before{content:"\f375"}.ion-android-clipboard:before{content:"\f376"}.ion-android-close:before{content:"\f2d7"}.ion-android-cloud:before{content:"\f37a"}.ion-android-cloud-circle:before{content:"\f377"}.ion-android-cloud-done:before{content:"\f378"}.ion-android-cloud-outline:before{content:"\f379"}.ion-android-color-palette:before{content:"\f37b"}.ion-android-compass:before{content:"\f37c"}.ion-android-contact:before{content:"\f2d8"}.ion-android-contacts:before{content:"\f2d9"}.ion-android-contract:before{content:"\f37d"}.ion-android-create:before{content:"\f37e"}.ion-android-delete:before{content:"\f37f"}.ion-android-desktop:before{content:"\f380"}.ion-android-document:before{content:"\f381"}.ion-android-done:before{content:"\f383"}.ion-android-done-all:before{content:"\f382"}.ion-android-download:before{content:"\f2dd"}.ion-android-drafts:before{content:"\f384"}.ion-android-exit:before{content:"\f385"}.ion-android-expand:before{content:"\f386"}.ion-android-favorite:before{content:"\f388"}.ion-android-favorite-outline:before{content:"\f387"}.ion-android-film:before{content:"\f389"}.ion-android-folder:before{content:"\f2e0"}.ion-android-folder-open:before{content:"\f38a"}.ion-android-funnel:before{content:"\f38b"}.ion-android-globe:before{content:"\f38c"}.ion-android-hand:before{content:"\f2e3"}.ion-android-hangout:before{content:"\f38d"}.ion-android-happy:before{content:"\f38e"}.ion-android-home:before{content:"\f38f"}.ion-android-image:before{content:"\f2e4"}.ion-android-laptop:before{content:"\f390"}.ion-android-list:before{content:"\f391"}.ion-android-locate:before{content:"\f2e9"}.ion-android-lock:before{content:"\f392"}.ion-android-mail:before{content:"\f2eb"}.ion-android-map:before{content:"\f393"}.ion-android-menu:before{content:"\f394"}.ion-android-microphone:before{content:"\f2ec"}.ion-android-microphone-off:before{content:"\f395"}.ion-android-more-horizontal:before{content:"\f396"}.ion-android-more-vertical:before{content:"\f397"}.ion-android-navigate:before{content:"\f398"}.ion-android-notifications:before{content:"\f39b"}.ion-android-notifications-none:before{content:"\f399"}.ion-android-notifications-off:before{content:"\f39a"}.ion-android-open:before{content:"\f39c"}.ion-android-options:before{content:"\f39d"}.ion-android-people:before{content:"\f39e"}.ion-android-person:before{content:"\f3a0"}.ion-android-person-add:before{content:"\f39f"}.ion-android-phone-landscape:before{content:"\f3a1"}.ion-android-phone-portrait:before{content:"\f3a2"}.ion-android-pin:before{content:"\f3a3"}.ion-android-plane:before{content:"\f3a4"}.ion-android-playstore:before{content:"\f2f0"}.ion-android-print:before{content:"\f3a5"}.ion-android-radio-button-off:before{content:"\f3a6"}.ion-android-radio-button-on:before{content:"\f3a7"}.ion-android-refresh:before{content:"\f3a8"}.ion-android-remove:before{content:"\f2f4"}.ion-android-remove-circle:before{content:"\f3a9"}.ion-android-restaurant:before{content:"\f3aa"}.ion-android-sad:before{content:"\f3ab"}.ion-android-search:before{content:"\f2f5"}.ion-android-send:before{content:"\f2f6"}.ion-android-settings:before{content:"\f2f7"}.ion-android-share:before{content:"\f2f8"}.ion-android-share-alt:before{content:"\f3ac"}.ion-android-star:before{content:"\f2fc"}.ion-android-star-half:before{content:"\f3ad"}.ion-android-star-outline:before{content:"\f3ae"}.ion-android-stopwatch:before{content:"\f2fd"}.ion-android-subway:before{content:"\f3af"}.ion-android-sunny:before{content:"\f3b0"}.ion-android-sync:before{content:"\f3b1"}.ion-android-textsms:before{content:"\f3b2"}.ion-android-time:before{content:"\f3b3"}.ion-android-train:before{content:"\f3b4"}.ion-android-unlock:before{content:"\f3b5"}.ion-android-upload:before{content:"\f3b6"}.ion-android-volume-down:before{content:"\f3b7"}.ion-android-volume-mute:before{content:"\f3b8"}.ion-android-volume-off:before{content:"\f3b9"}.ion-android-volume-up:before{content:"\f3ba"}.ion-android-walk:before{content:"\f3bb"}.ion-android-warning:before{content:"\f3bc"}.ion-android-watch:before{content:"\f3bd"}.ion-android-wifi:before{content:"\f305"}.ion-aperture:before{content:"\f313"}.ion-archive:before{content:"\f102"}.ion-arrow-down-a:before{content:"\f103"}.ion-arrow-down-b:before{content:"\f104"}.ion-arrow-down-c:before{content:"\f105"}.ion-arrow-expand:before{content:"\f25e"}.ion-arrow-graph-down-left:before{content:"\f25f"}.ion-arrow-graph-down-right:before{content:"\f260"}.ion-arrow-graph-up-left:before{content:"\f261"}.ion-arrow-graph-up-right:before{content:"\f262"}.ion-arrow-left-a:before{content:"\f106"}.ion-arrow-left-b:before{content:"\f107"}.ion-arrow-left-c:before{content:"\f108"}.ion-arrow-move:before{content:"\f263"}.ion-arrow-resize:before{content:"\f264"}.ion-arrow-return-left:before{content:"\f265"}.ion-arrow-return-right:before{content:"\f266"}.ion-arrow-right-a:before{content:"\f109"}.ion-arrow-right-b:before{content:"\f10a"}.ion-arrow-right-c:before{content:"\f10b"}.ion-arrow-shrink:before{content:"\f267"}.ion-arrow-swap:before{content:"\f268"}.ion-arrow-up-a:before{content:"\f10c"}.ion-arrow-up-b:before{content:"\f10d"}.ion-arrow-up-c:before{content:"\f10e"}.ion-asterisk:before{content:"\f314"}.ion-at:before{content:"\f10f"}.ion-backspace:before{content:"\f3bf"}.ion-backspace-outline:before{content:"\f3be"}.ion-bag:before{content:"\f110"}.ion-battery-charging:before{content:"\f111"}.ion-battery-empty:before{content:"\f112"}.ion-battery-full:before{content:"\f113"}.ion-battery-half:before{content:"\f114"}.ion-battery-low:before{content:"\f115"}.ion-beaker:before{content:"\f269"}.ion-beer:before{content:"\f26a"}.ion-bluetooth:before{content:"\f116"}.ion-bonfire:before{content:"\f315"}.ion-bookmark:before{content:"\f26b"}.ion-bowtie:before{content:"\f3c0"}.ion-briefcase:before{content:"\f26c"}.ion-bug:before{content:"\f2be"}.ion-calculator:before{content:"\f26d"}.ion-calendar:before{content:"\f117"}.ion-camera:before{content:"\f118"}.ion-card:before{content:"\f119"}.ion-cash:before{content:"\f316"}.ion-chatbox:before{content:"\f11b"}.ion-chatbox-working:before{content:"\f11a"}.ion-chatboxes:before{content:"\f11c"}.ion-chatbubble:before{content:"\f11e"}.ion-chatbubble-working:before{content:"\f11d"}.ion-chatbubbles:before{content:"\f11f"}.ion-checkmark:before{content:"\f122"}.ion-checkmark-circled:before{content:"\f120"}.ion-checkmark-round:before{content:"\f121"}.ion-chevron-down:before{content:"\f123"}.ion-chevron-left:before{content:"\f124"}.ion-chevron-right:before{content:"\f125"}.ion-chevron-up:before{content:"\f126"}.ion-clipboard:before{content:"\f127"}.ion-clock:before{content:"\f26e"}.ion-close:before{content:"\f12a"}.ion-close-circled:before{content:"\f128"}.ion-close-round:before{content:"\f129"}.ion-closed-captioning:before{content:"\f317"}.ion-cloud:before{content:"\f12b"}.ion-code:before{content:"\f271"}.ion-code-download:before{content:"\f26f"}.ion-code-working:before{content:"\f270"}.ion-coffee:before{content:"\f272"}.ion-compass:before{content:"\f273"}.ion-compose:before{content:"\f12c"}.ion-connection-bars:before{content:"\f274"}.ion-contrast:before{content:"\f275"}.ion-crop:before{content:"\f3c1"}.ion-cube:before{content:"\f318"}.ion-disc:before{content:"\f12d"}.ion-document:before{content:"\f12f"}.ion-document-text:before{content:"\f12e"}.ion-drag:before{content:"\f130"}.ion-earth:before{content:"\f276"}.ion-easel:before{content:"\f3c2"}.ion-edit:before{content:"\f2bf"}.ion-egg:before{content:"\f277"}.ion-eject:before{content:"\f131"}.ion-email:before{content:"\f132"}.ion-email-unread:before{content:"\f3c3"}.ion-erlenmeyer-flask:before{content:"\f3c5"}.ion-erlenmeyer-flask-bubbles:before{content:"\f3c4"}.ion-eye:before{content:"\f133"}.ion-eye-disabled:before{content:"\f306"}.ion-female:before{content:"\f278"}.ion-filing:before{content:"\f134"}.ion-film-marker:before{content:"\f135"}.ion-fireball:before{content:"\f319"}.ion-flag:before{content:"\f279"}.ion-flame:before{content:"\f31a"}.ion-flash:before{content:"\f137"}.ion-flash-off:before{content:"\f136"}.ion-folder:before{content:"\f139"}.ion-fork:before{content:"\f27a"}.ion-fork-repo:before{content:"\f2c0"}.ion-forward:before{content:"\f13a"}.ion-funnel:before{content:"\f31b"}.ion-gear-a:before{content:"\f13d"}.ion-gear-b:before{content:"\f13e"}.ion-grid:before{content:"\f13f"}.ion-hammer:before{content:"\f27b"}.ion-happy:before{content:"\f31c"}.ion-happy-outline:before{content:"\f3c6"}.ion-headphone:before{content:"\f140"}.ion-heart:before{content:"\f141"}.ion-heart-broken:before{content:"\f31d"}.ion-help:before{content:"\f143"}.ion-help-buoy:before{content:"\f27c"}.ion-help-circled:before{content:"\f142"}.ion-home:before{content:"\f144"}.ion-icecream:before{content:"\f27d"}.ion-image:before{content:"\f147"}.ion-images:before{content:"\f148"}.ion-information:before{content:"\f14a"}.ion-information-circled:before{content:"\f149"}.ion-ionic:before{content:"\f14b"}.ion-ios-alarm:before{content:"\f3c8"}.ion-ios-alarm-outline:before{content:"\f3c7"}.ion-ios-albums:before{content:"\f3ca"}.ion-ios-albums-outline:before{content:"\f3c9"}.ion-ios-americanfootball:before{content:"\f3cc"}.ion-ios-americanfootball-outline:before{content:"\f3cb"}.ion-ios-analytics:before{content:"\f3ce"}.ion-ios-analytics-outline:before{content:"\f3cd"}.ion-ios-arrow-back:before{content:"\f3cf"}.ion-ios-arrow-down:before{content:"\f3d0"}.ion-ios-arrow-forward:before{content:"\f3d1"}.ion-ios-arrow-left:before{content:"\f3d2"}.ion-ios-arrow-right:before{content:"\f3d3"}.ion-ios-arrow-thin-down:before{content:"\f3d4"}.ion-ios-arrow-thin-left:before{content:"\f3d5"}.ion-ios-arrow-thin-right:before{content:"\f3d6"}.ion-ios-arrow-thin-up:before{content:"\f3d7"}.ion-ios-arrow-up:before{content:"\f3d8"}.ion-ios-at:before{content:"\f3da"}.ion-ios-at-outline:before{content:"\f3d9"}.ion-ios-barcode:before{content:"\f3dc"}.ion-ios-barcode-outline:before{content:"\f3db"}.ion-ios-baseball:before{content:"\f3de"}.ion-ios-baseball-outline:before{content:"\f3dd"}.ion-ios-basketball:before{content:"\f3e0"}.ion-ios-basketball-outline:before{content:"\f3df"}.ion-ios-bell:before{content:"\f3e2"}.ion-ios-bell-outline:before{content:"\f3e1"}.ion-ios-body:before{content:"\f3e4"}.ion-ios-body-outline:before{content:"\f3e3"}.ion-ios-bolt:before{content:"\f3e6"}.ion-ios-bolt-outline:before{content:"\f3e5"}.ion-ios-book:before{content:"\f3e8"}.ion-ios-book-outline:before{content:"\f3e7"}.ion-ios-bookmarks:before{content:"\f3ea"}.ion-ios-bookmarks-outline:before{content:"\f3e9"}.ion-ios-box:before{content:"\f3ec"}.ion-ios-box-outline:before{content:"\f3eb"}.ion-ios-briefcase:before{content:"\f3ee"}.ion-ios-briefcase-outline:before{content:"\f3ed"}.ion-ios-browsers:before{content:"\f3f0"}.ion-ios-browsers-outline:before{content:"\f3ef"}.ion-ios-calculator:before{content:"\f3f2"}.ion-ios-calculator-outline:before{content:"\f3f1"}.ion-ios-calendar:before{content:"\f3f4"}.ion-ios-calendar-outline:before{content:"\f3f3"}.ion-ios-camera:before{content:"\f3f6"}.ion-ios-camera-outline:before{content:"\f3f5"}.ion-ios-cart:before{content:"\f3f8"}.ion-ios-cart-outline:before{content:"\f3f7"}.ion-ios-chatboxes:before{content:"\f3fa"}.ion-ios-chatboxes-outline:before{content:"\f3f9"}.ion-ios-chatbubble:before{content:"\f3fc"}.ion-ios-chatbubble-outline:before{content:"\f3fb"}.ion-ios-checkmark:before{content:"\f3ff"}.ion-ios-checkmark-empty:before{content:"\f3fd"}.ion-ios-checkmark-outline:before{content:"\f3fe"}.ion-ios-circle-filled:before{content:"\f400"}.ion-ios-circle-outline:before{content:"\f401"}.ion-ios-clock:before{content:"\f403"}.ion-ios-clock-outline:before{content:"\f402"}.ion-ios-close:before{content:"\f406"}.ion-ios-close-empty:before{content:"\f404"}.ion-ios-close-outline:before{content:"\f405"}.ion-ios-cloud:before{content:"\f40c"}.ion-ios-cloud-download:before{content:"\f408"}.ion-ios-cloud-download-outline:before{content:"\f407"}.ion-ios-cloud-outline:before{content:"\f409"}.ion-ios-cloud-upload:before{content:"\f40b"}.ion-ios-cloud-upload-outline:before{content:"\f40a"}.ion-ios-cloudy:before{content:"\f410"}.ion-ios-cloudy-night:before{content:"\f40e"}.ion-ios-cloudy-night-outline:before{content:"\f40d"}.ion-ios-cloudy-outline:before{content:"\f40f"}.ion-ios-cog:before{content:"\f412"}.ion-ios-cog-outline:before{content:"\f411"}.ion-ios-color-filter:before{content:"\f414"}.ion-ios-color-filter-outline:before{content:"\f413"}.ion-ios-color-wand:before{content:"\f416"}.ion-ios-color-wand-outline:before{content:"\f415"}.ion-ios-compose:before{content:"\f418"}.ion-ios-compose-outline:before{content:"\f417"}.ion-ios-contact:before{content:"\f41a"}.ion-ios-contact-outline:before{content:"\f419"}.ion-ios-copy:before{content:"\f41c"}.ion-ios-copy-outline:before{content:"\f41b"}.ion-ios-crop:before{content:"\f41e"}.ion-ios-crop-strong:before{content:"\f41d"}.ion-ios-download:before{content:"\f420"}.ion-ios-download-outline:before{content:"\f41f"}.ion-ios-drag:before{content:"\f421"}.ion-ios-email:before{content:"\f423"}.ion-ios-email-outline:before{content:"\f422"}.ion-ios-eye:before{content:"\f425"}.ion-ios-eye-outline:before{content:"\f424"}.ion-ios-fastforward:before{content:"\f427"}.ion-ios-fastforward-outline:before{content:"\f426"}.ion-ios-filing:before{content:"\f429"}.ion-ios-filing-outline:before{content:"\f428"}.ion-ios-film:before{content:"\f42b"}.ion-ios-film-outline:before{content:"\f42a"}.ion-ios-flag:before{content:"\f42d"}.ion-ios-flag-outline:before{content:"\f42c"}.ion-ios-flame:before{content:"\f42f"}.ion-ios-flame-outline:before{content:"\f42e"}.ion-ios-flask:before{content:"\f431"}.ion-ios-flask-outline:before{content:"\f430"}.ion-ios-flower:before{content:"\f433"}.ion-ios-flower-outline:before{content:"\f432"}.ion-ios-folder:before{content:"\f435"}.ion-ios-folder-outline:before{content:"\f434"}.ion-ios-football:before{content:"\f437"}.ion-ios-football-outline:before{content:"\f436"}.ion-ios-game-controller-a:before{content:"\f439"}.ion-ios-game-controller-a-outline:before{content:"\f438"}.ion-ios-game-controller-b:before{content:"\f43b"}.ion-ios-game-controller-b-outline:before{content:"\f43a"}.ion-ios-gear:before{content:"\f43d"}.ion-ios-gear-outline:before{content:"\f43c"}.ion-ios-glasses:before{content:"\f43f"}.ion-ios-glasses-outline:before{content:"\f43e"}.ion-ios-grid-view:before{content:"\f441"}.ion-ios-grid-view-outline:before{content:"\f440"}.ion-ios-heart:before{content:"\f443"}.ion-ios-heart-outline:before{content:"\f442"}.ion-ios-help:before{content:"\f446"}.ion-ios-help-empty:before{content:"\f444"}.ion-ios-help-outline:before{content:"\f445"}.ion-ios-home:before{content:"\f448"}.ion-ios-home-outline:before{content:"\f447"}.ion-ios-infinite:before{content:"\f44a"}.ion-ios-infinite-outline:before{content:"\f449"}.ion-ios-information:before{content:"\f44d"}.ion-ios-information-empty:before{content:"\f44b"}.ion-ios-information-outline:before{content:"\f44c"}.ion-ios-ionic-outline:before{content:"\f44e"}.ion-ios-keypad:before{content:"\f450"}.ion-ios-keypad-outline:before{content:"\f44f"}.ion-ios-lightbulb:before{content:"\f452"}.ion-ios-lightbulb-outline:before{content:"\f451"}.ion-ios-list:before{content:"\f454"}.ion-ios-list-outline:before{content:"\f453"}.ion-ios-location:before{content:"\f456"}.ion-ios-location-outline:before{content:"\f455"}.ion-ios-locked:before{content:"\f458"}.ion-ios-locked-outline:before{content:"\f457"}.ion-ios-loop:before{content:"\f45a"}.ion-ios-loop-strong:before{content:"\f459"}.ion-ios-medical:before{content:"\f45c"}.ion-ios-medical-outline:before{content:"\f45b"}.ion-ios-medkit:before{content:"\f45e"}.ion-ios-medkit-outline:before{content:"\f45d"}.ion-ios-mic:before{content:"\f461"}.ion-ios-mic-off:before{content:"\f45f"}.ion-ios-mic-outline:before{content:"\f460"}.ion-ios-minus:before{content:"\f464"}.ion-ios-minus-empty:before{content:"\f462"}.ion-ios-minus-outline:before{content:"\f463"}.ion-ios-monitor:before{content:"\f466"}.ion-ios-monitor-outline:before{content:"\f465"}.ion-ios-moon:before{content:"\f468"}.ion-ios-moon-outline:before{content:"\f467"}.ion-ios-more:before{content:"\f46a"}.ion-ios-more-outline:before{content:"\f469"}.ion-ios-musical-note:before{content:"\f46b"}.ion-ios-musical-notes:before{content:"\f46c"}.ion-ios-navigate:before{content:"\f46e"}.ion-ios-navigate-outline:before{content:"\f46d"}.ion-ios-nutrition:before{content:"\f470"}.ion-ios-nutrition-outline:before{content:"\f46f"}.ion-ios-paper:before{content:"\f472"}.ion-ios-paper-outline:before{content:"\f471"}.ion-ios-paperplane:before{content:"\f474"}.ion-ios-paperplane-outline:before{content:"\f473"}.ion-ios-partlysunny:before{content:"\f476"}.ion-ios-partlysunny-outline:before{content:"\f475"}.ion-ios-pause:before{content:"\f478"}.ion-ios-pause-outline:before{content:"\f477"}.ion-ios-paw:before{content:"\f47a"}.ion-ios-paw-outline:before{content:"\f479"}.ion-ios-people:before{content:"\f47c"}.ion-ios-people-outline:before{content:"\f47b"}.ion-ios-person:before{content:"\f47e"}.ion-ios-person-outline:before{content:"\f47d"}.ion-ios-personadd:before{content:"\f480"}.ion-ios-personadd-outline:before{content:"\f47f"}.ion-ios-photos:before{content:"\f482"}.ion-ios-photos-outline:before{content:"\f481"}.ion-ios-pie:before{content:"\f484"}.ion-ios-pie-outline:before{content:"\f483"}.ion-ios-pint:before{content:"\f486"}.ion-ios-pint-outline:before{content:"\f485"}.ion-ios-play:before{content:"\f488"}.ion-ios-play-outline:before{content:"\f487"}.ion-ios-plus:before{content:"\f48b"}.ion-ios-plus-empty:before{content:"\f489"}.ion-ios-plus-outline:before{content:"\f48a"}.ion-ios-pricetag:before{content:"\f48d"}.ion-ios-pricetag-outline:before{content:"\f48c"}.ion-ios-pricetags:before{content:"\f48f"}.ion-ios-pricetags-outline:before{content:"\f48e"}.ion-ios-printer:before{content:"\f491"}.ion-ios-printer-outline:before{content:"\f490"}.ion-ios-pulse:before{content:"\f493"}.ion-ios-pulse-strong:before{content:"\f492"}.ion-ios-rainy:before{content:"\f495"}.ion-ios-rainy-outline:before{content:"\f494"}.ion-ios-recording:before{content:"\f497"}.ion-ios-recording-outline:before{content:"\f496"}.ion-ios-redo:before{content:"\f499"}.ion-ios-redo-outline:before{content:"\f498"}.ion-ios-refresh:before{content:"\f49c"}.ion-ios-refresh-empty:before{content:"\f49a"}.ion-ios-refresh-outline:before{content:"\f49b"}.ion-ios-reload:before{content:"\f49d"}.ion-ios-reverse-camera:before{content:"\f49f"}.ion-ios-reverse-camera-outline:before{content:"\f49e"}.ion-ios-rewind:before{content:"\f4a1"}.ion-ios-rewind-outline:before{content:"\f4a0"}.ion-ios-rose:before{content:"\f4a3"}.ion-ios-rose-outline:before{content:"\f4a2"}.ion-ios-search:before{content:"\f4a5"}.ion-ios-search-strong:before{content:"\f4a4"}.ion-ios-settings:before{content:"\f4a7"}.ion-ios-settings-strong:before{content:"\f4a6"}.ion-ios-shuffle:before{content:"\f4a9"}.ion-ios-shuffle-strong:before{content:"\f4a8"}.ion-ios-skipbackward:before{content:"\f4ab"}.ion-ios-skipbackward-outline:before{content:"\f4aa"}.ion-ios-skipforward:before{content:"\f4ad"}.ion-ios-skipforward-outline:before{content:"\f4ac"}.ion-ios-snowy:before{content:"\f4ae"}.ion-ios-speedometer:before{content:"\f4b0"}.ion-ios-speedometer-outline:before{content:"\f4af"}.ion-ios-star:before{content:"\f4b3"}.ion-ios-star-half:before{content:"\f4b1"}.ion-ios-star-outline:before{content:"\f4b2"}.ion-ios-stopwatch:before{content:"\f4b5"}.ion-ios-stopwatch-outline:before{content:"\f4b4"}.ion-ios-sunny:before{content:"\f4b7"}.ion-ios-sunny-outline:before{content:"\f4b6"}.ion-ios-telephone:before{content:"\f4b9"}.ion-ios-telephone-outline:before{content:"\f4b8"}.ion-ios-tennisball:before{content:"\f4bb"}.ion-ios-tennisball-outline:before{content:"\f4ba"}.ion-ios-thunderstorm:before{content:"\f4bd"}.ion-ios-thunderstorm-outline:before{content:"\f4bc"}.ion-ios-time:before{content:"\f4bf"}.ion-ios-time-outline:before{content:"\f4be"}.ion-ios-timer:before{content:"\f4c1"}.ion-ios-timer-outline:before{content:"\f4c0"}.ion-ios-toggle:before{content:"\f4c3"}.ion-ios-toggle-outline:before{content:"\f4c2"}.ion-ios-trash:before{content:"\f4c5"}.ion-ios-trash-outline:before{content:"\f4c4"}.ion-ios-undo:before{content:"\f4c7"}.ion-ios-undo-outline:before{content:"\f4c6"}.ion-ios-unlocked:before{content:"\f4c9"}.ion-ios-unlocked-outline:before{content:"\f4c8"}.ion-ios-upload:before{content:"\f4cb"}.ion-ios-upload-outline:before{content:"\f4ca"}.ion-ios-videocam:before{content:"\f4cd"}.ion-ios-videocam-outline:before{content:"\f4cc"}.ion-ios-volume-high:before{content:"\f4ce"}.ion-ios-volume-low:before{content:"\f4cf"}.ion-ios-wineglass:before{content:"\f4d1"}.ion-ios-wineglass-outline:before{content:"\f4d0"}.ion-ios-world:before{content:"\f4d3"}.ion-ios-world-outline:before{content:"\f4d2"}.ion-ipad:before{content:"\f1f9"}.ion-iphone:before{content:"\f1fa"}.ion-ipod:before{content:"\f1fb"}.ion-jet:before{content:"\f295"}.ion-key:before{content:"\f296"}.ion-knife:before{content:"\f297"}.ion-laptop:before{content:"\f1fc"}.ion-leaf:before{content:"\f1fd"}.ion-levels:before{content:"\f298"}.ion-lightbulb:before{content:"\f299"}.ion-link:before{content:"\f1fe"}.ion-load-a:before{content:"\f29a"}.ion-load-b:before{content:"\f29b"}.ion-load-c:before{content:"\f29c"}.ion-load-d:before{content:"\f29d"}.ion-location:before{content:"\f1ff"}.ion-lock-combination:before{content:"\f4d4"}.ion-locked:before{content:"\f200"}.ion-log-in:before{content:"\f29e"}.ion-log-out:before{content:"\f29f"}.ion-loop:before{content:"\f201"}.ion-magnet:before{content:"\f2a0"}.ion-male:before{content:"\f2a1"}.ion-man:before{content:"\f202"}.ion-map:before{content:"\f203"}.ion-medkit:before{content:"\f2a2"}.ion-merge:before{content:"\f33f"}.ion-mic-a:before{content:"\f204"}.ion-mic-b:before{content:"\f205"}.ion-mic-c:before{content:"\f206"}.ion-minus:before{content:"\f209"}.ion-minus-circled:before{content:"\f207"}.ion-minus-round:before{content:"\f208"}.ion-model-s:before{content:"\f2c1"}.ion-monitor:before{content:"\f20a"}.ion-more:before{content:"\f20b"}.ion-mouse:before{content:"\f340"}.ion-music-note:before{content:"\f20c"}.ion-navicon:before{content:"\f20e"}.ion-navicon-round:before{content:"\f20d"}.ion-navigate:before{content:"\f2a3"}.ion-network:before{content:"\f341"}.ion-no-smoking:before{content:"\f2c2"}.ion-nuclear:before{content:"\f2a4"}.ion-outlet:before{content:"\f342"}.ion-paintbrush:before{content:"\f4d5"}.ion-paintbucket:before{content:"\f4d6"}.ion-paper-airplane:before{content:"\f2c3"}.ion-paperclip:before{content:"\f20f"}.ion-pause:before{content:"\f210"}.ion-person:before{content:"\f213"}.ion-person-add:before{content:"\f211"}.ion-person-stalker:before{content:"\f212"}.ion-pie-graph:before{content:"\f2a5"}.ion-pin:before{content:"\f2a6"}.ion-pinpoint:before{content:"\f2a7"}.ion-pizza:before{content:"\f2a8"}.ion-plane:before{content:"\f214"}.ion-planet:before{content:"\f343"}.ion-play:before{content:"\f215"}.ion-playstation:before{content:"\f30a"}.ion-plus:before{content:"\f218"}.ion-plus-circled:before{content:"\f216"}.ion-plus-round:before{content:"\f217"}.ion-podium:before{content:"\f344"}.ion-pound:before{content:"\f219"}.ion-power:before{content:"\f2a9"}.ion-pricetag:before{content:"\f2aa"}.ion-pricetags:before{content:"\f2ab"}.ion-printer:before{content:"\f21a"}.ion-pull-request:before{content:"\f345"}.ion-qr-scanner:before{content:"\f346"}.ion-quote:before{content:"\f347"}.ion-radio-waves:before{content:"\f2ac"}.ion-record:before{content:"\f21b"}.ion-refresh:before{content:"\f21c"}.ion-reply:before{content:"\f21e"}.ion-reply-all:before{content:"\f21d"}.ion-ribbon-a:before{content:"\f348"}.ion-ribbon-b:before{content:"\f349"}.ion-sad:before{content:"\f34a"}.ion-sad-outline:before{content:"\f4d7"}.ion-scissors:before{content:"\f34b"}.ion-search:before{content:"\f21f"}.ion-settings:before{content:"\f2ad"}.ion-share:before{content:"\f220"}.ion-shuffle:before{content:"\f221"}.ion-skip-backward:before{content:"\f222"}.ion-skip-forward:before{content:"\f223"}.ion-social-android:before{content:"\f225"}.ion-social-android-outline:before{content:"\f224"}.ion-social-angular:before{content:"\f4d9"}.ion-social-angular-outline:before{content:"\f4d8"}.ion-social-apple:before{content:"\f227"}.ion-social-apple-outline:before{content:"\f226"}.ion-social-bitcoin:before{content:"\f2af"}.ion-social-bitcoin-outline:before{content:"\f2ae"}.ion-social-buffer:before{content:"\f229"}.ion-social-buffer-outline:before{content:"\f228"}.ion-social-chrome:before{content:"\f4db"}.ion-social-chrome-outline:before{content:"\f4da"}.ion-social-codepen:before{content:"\f4dd"}.ion-social-codepen-outline:before{content:"\f4dc"}.ion-social-css3:before{content:"\f4df"}.ion-social-css3-outline:before{content:"\f4de"}.ion-social-designernews:before{content:"\f22b"}.ion-social-designernews-outline:before{content:"\f22a"}.ion-social-dribbble:before{content:"\f22d"}.ion-social-dribbble-outline:before{content:"\f22c"}.ion-social-dropbox:before{content:"\f22f"}.ion-social-dropbox-outline:before{content:"\f22e"}.ion-social-euro:before{content:"\f4e1"}.ion-social-euro-outline:before{content:"\f4e0"}.ion-social-facebook:before{content:"\f231"}.ion-social-facebook-outline:before{content:"\f230"}.ion-social-foursquare:before{content:"\f34d"}.ion-social-foursquare-outline:before{content:"\f34c"}.ion-social-freebsd-devil:before{content:"\f2c4"}.ion-social-github:before{content:"\f233"}.ion-social-github-outline:before{content:"\f232"}.ion-social-google:before{content:"\f34f"}.ion-social-google-outline:before{content:"\f34e"}.ion-social-googleplus:before{content:"\f235"}.ion-social-googleplus-outline:before{content:"\f234"}.ion-social-hackernews:before{content:"\f237"}.ion-social-hackernews-outline:before{content:"\f236"}.ion-social-html5:before{content:"\f4e3"}.ion-social-html5-outline:before{content:"\f4e2"}.ion-social-instagram:before{content:"\f351"}.ion-social-instagram-outline:before{content:"\f350"}.ion-social-javascript:before{content:"\f4e5"}.ion-social-javascript-outline:before{content:"\f4e4"}.ion-social-linkedin:before{content:"\f239"}.ion-social-linkedin-outline:before{content:"\f238"}.ion-social-markdown:before{content:"\f4e6"}.ion-social-nodejs:before{content:"\f4e7"}.ion-social-octocat:before{content:"\f4e8"}.ion-social-pinterest:before{content:"\f2b1"}.ion-social-pinterest-outline:before{content:"\f2b0"}.ion-social-python:before{content:"\f4e9"}.ion-social-reddit:before{content:"\f23b"}.ion-social-reddit-outline:before{content:"\f23a"}.ion-social-rss:before{content:"\f23d"}.ion-social-rss-outline:before{content:"\f23c"}.ion-social-sass:before{content:"\f4ea"}.ion-social-skype:before{content:"\f23f"}.ion-social-skype-outline:before{content:"\f23e"}.ion-social-snapchat:before{content:"\f4ec"}.ion-social-snapchat-outline:before{content:"\f4eb"}.ion-social-tumblr:before{content:"\f241"}.ion-social-tumblr-outline:before{content:"\f240"}.ion-social-tux:before{content:"\f2c5"}.ion-social-twitch:before{content:"\f4ee"}.ion-social-twitch-outline:before{content:"\f4ed"}.ion-social-twitter:before{content:"\f243"}.ion-social-twitter-outline:before{content:"\f242"}.ion-social-usd:before{content:"\f353"}.ion-social-usd-outline:before{content:"\f352"}.ion-social-vimeo:before{content:"\f245"}.ion-social-vimeo-outline:before{content:"\f244"}.ion-social-whatsapp:before{content:"\f4f0"}.ion-social-whatsapp-outline:before{content:"\f4ef"}.ion-social-windows:before{content:"\f247"}.ion-social-windows-outline:before{content:"\f246"}.ion-social-wordpress:before{content:"\f249"}.ion-social-wordpress-outline:before{content:"\f248"}.ion-social-yahoo:before{content:"\f24b"}.ion-social-yahoo-outline:before{content:"\f24a"}.ion-social-yen:before{content:"\f4f2"}.ion-social-yen-outline:before{content:"\f4f1"}.ion-social-youtube:before{content:"\f24d"}.ion-social-youtube-outline:before{content:"\f24c"}.ion-soup-can:before{content:"\f4f4"}.ion-soup-can-outline:before{content:"\f4f3"}.ion-speakerphone:before{content:"\f2b2"}.ion-speedometer:before{content:"\f2b3"}.ion-spoon:before{content:"\f2b4"}.ion-star:before{content:"\f24e"}.ion-stats-bars:before{content:"\f2b5"}.ion-steam:before{content:"\f30b"}.ion-stop:before{content:"\f24f"}.ion-thermometer:before{content:"\f2b6"}.ion-thumbsdown:before{content:"\f250"}.ion-thumbsup:before{content:"\f251"}.ion-toggle:before{content:"\f355"}.ion-toggle-filled:before{content:"\f354"}.ion-transgender:before{content:"\f4f5"}.ion-trash-a:before{content:"\f252"}.ion-trash-b:before{content:"\f253"}.ion-trophy:before{content:"\f356"}.ion-tshirt:before{content:"\f4f7"}.ion-tshirt-outline:before{content:"\f4f6"}.ion-umbrella:before{content:"\f2b7"}.ion-university:before{content:"\f357"}.ion-unlocked:before{content:"\f254"}.ion-upload:before{content:"\f255"}.ion-usb:before{content:"\f2b8"}.ion-videocamera:before{content:"\f256"}.ion-volume-high:before{content:"\f257"}.ion-volume-low:before{content:"\f258"}.ion-volume-medium:before{content:"\f259"}.ion-volume-mute:before{content:"\f25a"}.ion-wand:before{content:"\f358"}.ion-waterdrop:before{content:"\f25b"}.ion-wifi:before{content:"\f25c"}.ion-wineglass:before{content:"\f2b9"}.ion-woman:before{content:"\f25d"}.ion-wrench:before{content:"\f2ba"}.ion-xbox:before{content:"\f30c"} diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot new file mode 100644 index 0000000000000000000000000000000000000000..92a3f20a39267ae7f45144f412a995a663730360 GIT binary patch literal 120724 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

@FHUiu&=5w(f-3S1h*&)&h2 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 + + + + + Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014 + By Adam Bradley + Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c4e4632486d863337c1c73478ddb3c20726c55a0 GIT binary patch literal 188508 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

8E#xmG!w{$tO5=<$!EHPzZXo}IBc1_sqA&q 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 literal 0 HcmV?d00001 diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff new file mode 100644 index 0000000000000000000000000000000000000000..5f3a14e0a5ca6d20cc4fac708979e807b0d51bc3 GIT binary patch literal 67904 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

  • F(S(gioucm()OHA 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 E0LEz`i~s-t literal 0 HcmV?d00001 diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js new file mode 100644 index 0000000..234f9b3 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js @@ -0,0 +1,2 @@ +/*! pace 1.0.2 */ +(function(){var 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=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
    \n
    \n
    \n
    ',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e;e=[];for(d in b.prototype)try{e.push(null==a[d]&&"function"!=typeof b[d]?"function"==typeof Object.defineProperty?Object.defineProperty(a,d,{get:function(){return b.prototype[d]},configurable:!0,enumerable:!0}):a[d]=b.prototype[d]:void 0)}catch(f){c=f}return e},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(["pace"],function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css new file mode 100644 index 0000000..6c31e44 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css @@ -0,0 +1,111 @@ +/* This is a compiled file, you should be editing the file in the templates directory */ +.pace { + -webkit-pointer-events: none; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: #2299dd; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 2px; +} + +.pace .pace-progress-inner { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #2299dd, 0 0 5px #2299dd; + opacity: 1.0; + -webkit-transform: rotate(3deg) translate(0px, -4px); + -moz-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + -o-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +.pace .pace-activity { + display: block; + position: fixed; + z-index: 2000; + top: 15px; + right: 15px; + width: 14px; + height: 14px; + border: solid 2px transparent; + border-top-color: #2299dd; + border-left-color: #2299dd; + 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; +} + +@-webkit-keyframes pace-spinner { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@-moz-keyframes pace-spinner { + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@-o-keyframes pace-spinner { + 0% { + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@-ms-keyframes pace-spinner { + 0% { + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes pace-spinner { + 0% { + transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css new file mode 100644 index 0000000..5dd276b --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css @@ -0,0 +1,418 @@ +.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; + } +} diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js new file mode 100644 index 0000000..0fd276d --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js @@ -0,0 +1,1658 @@ +/** + * @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; + +})); diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000..5b96335 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map new file mode 100644 index 0000000..0ae3de5 --- /dev/null +++ b/xxl-job/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","dist/css/bootstrap.css","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"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A,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,EFCJ,yBCzEC,Ub2zDC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UcnxDI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GFUJ,yBClFC,Ubo+DC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,Uc57DI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GFmBJ,0BC3FC,Ub6oEC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UcrmEI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GCjEJ,MACE,iBAAA,YADF,uBAQI,SAAA,OACA,QAAA,aACA,MAAA,KAKA,sBf+xEJ,sBe9xEM,SAAA,OACA,QAAA,WACA,MAAA,KAKN,QACE,YAAA,IACA,eAAA,IACA,MAAA,KACA,WAAA,KAGF,GACE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","sourcesContent":["/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable\n\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// 1. Remove the bottom border in Chrome 57- and Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n//\n\nabbr[title] {\n border-bottom: none; // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n -moz-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n -webkit-box-shadow: none !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n -ms-touch-action: manipulation;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n -o-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n -o-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n -o-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@-o-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n -webkit-background-size: 40px 40px;\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out, -o-transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: -webkit-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out, -o-transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable declaration-no-important, selector-no-qualifying-type\n\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important; // Black prints faster: h5bp.com/s\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n}\n","// stylelint-disable value-list-comma-newline-after, value-list-comma-space-after, indentation, declaration-colon-newline-after, font-family-no-missing-generic-family-keyword\n\n//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n//
    Star\n\n// Import the fonts\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"@{icon-font-path}@{icon-font-name}.eot\");\n src: url(\"@{icon-font-path}@{icon-font-name}.eot?#iefix\") format(\"embedded-opentype\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff2\") format(\"woff2\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff\") format(\"woff\"),\n url(\"@{icon-font-path}@{icon-font-name}.ttf\") format(\"truetype\"),\n url(\"@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}\") format(\"svg\");\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// https://getbootstrap.com/docs/3.4/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: https://a11yproject.com/posts/how-to-hide-content\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// stylelint-disable indentation, property-no-vendor-prefix, selector-no-vendor-prefix\n\n// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n word-wrap: break-word;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // WebKit-specific. Other browsers will keep their default outline style.\n // (Initially tried to also force default via `outline: initial`,\n // but that seems to erroneously remove the outline in Firefox altogether.)\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// stylelint-disable media-feature-name-no-vendor-prefix, media-feature-parentheses-space-inside, media-feature-name-no-unknown, indentation, at-rule-name-space-after\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","// stylelint-disable selector-list-comma-newline-after, selector-no-qualifying-type\n\n//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: 400;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n padding: .2em;\n background-color: @state-warning-bg;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: \"\\2014 \\00A0\"; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: \"\"; }\n &:after {\n content: \"\\00A0 \\2014\"; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n color: @pre-color;\n word-break: break-all;\n word-wrap: break-word;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n padding-right: ceil((@gutter / 2));\n padding-left: floor((@gutter / 2));\n margin-right: auto;\n margin-left: auto;\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-right: floor((@gutter / -2));\n margin-left: ceil((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-right: floor((@grid-gutter-width / 2));\n padding-left: ceil((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","// stylelint-disable selector-max-type, selector-max-compound-selectors, selector-no-qualifying-type\n\n//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n\n // Table cell sizing\n //\n // Reset default table behavior\n\n col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-column;\n float: none;\n }\n\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-cell;\n float: none;\n }\n }\n}\n\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\n\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n min-height: .01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n overflow-x: auto;\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * .75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type, property-no-vendor-prefix, media-feature-name-no-vendor-prefix\n\n//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: 700;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\ninput[type=\"search\"] {\n // Override content-box in Normalize (* isn't specific enough)\n .box-sizing(border-box);\n\n // Search inputs in iOS\n //\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n -webkit-appearance: none;\n appearance: none;\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n\n // Apply same disabled cursor tweak as for inputs\n // Some special care is needed because