From 671cfdd4865a4481939edaaf7066b2bd279d8b1a Mon Sep 17 00:00:00 2001 From: Anton Romanov Date: Fri, 4 May 2018 17:08:19 +0400 Subject: [PATCH] add java code --- build.gradle | 127 +++++ checkstyle.xml | 172 ++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54706 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 169 ++++++ gradlew.bat | 84 +++ papers.html | 112 ---- .../java/ru/ulstu/NgTrackerApplication.java | 14 + .../commit/controller/CommitController.java | 33 ++ .../ru/ulstu/commit/model/CommitListDto.java | 53 ++ .../ulstu/commit/service/CommitService.java | 62 +++ .../configuration/ApplicationProperties.java | 41 ++ .../configuration/AsyncConfiguration.java | 39 ++ .../ru/ulstu/configuration/Constants.java | 20 + .../ControllersConfiguration.java | 24 + .../HttpListenerConfiguration.java | 30 ++ .../configuration/JacksonConfiguration.java | 20 + .../MailTemplateConfiguration.java | 21 + .../ulstu/configuration/MvcConfiguration.java | 24 + .../PasswordEncoderConfiguration.java | 13 + .../configuration/SecurityConfiguration.java | 118 +++++ .../configuration/SwaggerConfiguration.java | 23 + .../core/controller/AdviceController.java | 99 ++++ .../core/error/EntityIdIsNullException.java | 6 + .../ru/ulstu/core/error/OdinException.java | 7 + .../ru/ulstu/core/error/XlsLoadException.java | 7 + .../ulstu/core/error/XlsParseException.java | 7 + .../ulstu/core/jpa/OffsetablePageRequest.java | 97 ++++ .../java/ru/ulstu/core/model/BaseEntity.java | 82 +++ .../ru/ulstu/core/model/ErrorConstants.java | 38 ++ .../java/ru/ulstu/core/model/TreeDto.java | 34 ++ .../java/ru/ulstu/core/model/TreeEntity.java | 16 + .../model/response/ControllerResponse.java | 24 + .../response/ControllerResponseError.java | 25 + .../core/model/response/PageableItems.java | 21 + .../ulstu/core/model/response/Response.java | 16 + .../core/model/response/ResponseExtended.java | 12 + .../repository/JpaDetachableRepository.java | 11 + .../JpaDetachableRepositoryImpl.java | 22 + .../ru/ulstu/core/service/TreeService.java | 31 ++ .../core/service/XlsDocumentBuilder.java | 207 ++++++++ .../java/ru/ulstu/core/util/DateUtils.java | 46 ++ .../java/ru/ulstu/core/util/NumberUtils.java | 17 + .../ru/ulstu/core/util/StreamApiUtils.java | 12 + .../ulstu/odin/controller/OdinController.java | 37 ++ .../ru/ulstu/odin/model/OdinBooleanField.java | 9 + .../ulstu/odin/model/OdinCollectionField.java | 28 + .../ru/ulstu/odin/model/OdinDateField.java | 18 + .../java/ru/ulstu/odin/model/OdinDto.java | 14 + .../java/ru/ulstu/odin/model/OdinField.java | 154 ++++++ .../ru/ulstu/odin/model/OdinMetadata.java | 26 + .../ru/ulstu/odin/model/OdinNumericField.java | 30 ++ .../ru/ulstu/odin/model/OdinObjectField.java | 26 + .../ru/ulstu/odin/model/OdinStringField.java | 37 ++ .../java/ru/ulstu/odin/model/OdinVoid.java | 18 + .../odin/model/annotation/OdinCaption.java | 14 + .../ulstu/odin/model/annotation/OdinDate.java | 23 + .../odin/model/annotation/OdinNumeric.java | 18 + .../odin/model/annotation/OdinReadOnly.java | 13 + .../odin/model/annotation/OdinString.java | 23 + .../odin/model/annotation/OdinVisible.java | 23 + .../ru/ulstu/odin/service/OdinService.java | 101 ++++ .../controller/OdinExampleController.java | 4 + .../odinexample/model/OdinExampleDto.java | 4 + .../odinexample/model/OdinExampleListDto.java | 103 ++++ .../service/OdinExampleService.java | 4 + .../user/component/IpAddressResolver.java | 23 + .../component/UserSessionLoginHandler.java | 45 ++ .../component/UserSessionLogoutHandler.java | 49 ++ .../ulstu/user/controller/UserController.java | 158 ++++++ .../ulstu/user/error/UserActivationError.java | 7 + .../user/error/UserEmailExistsException.java | 7 + .../user/error/UserIdExistsException.java | 6 + .../user/error/UserIsUndeadException.java | 7 + .../user/error/UserLoginExistsException.java | 7 + .../user/error/UserNotActivatedException.java | 6 + .../user/error/UserNotFoundException.java | 7 + ...rPasswordsNotValidOrNotMatchException.java | 6 + .../ulstu/user/error/UserResetKeyError.java | 7 + src/main/java/ru/ulstu/user/model/User.java | 170 ++++++ .../java/ru/ulstu/user/model/UserDto.java | 193 +++++++ .../java/ru/ulstu/user/model/UserListDto.java | 76 +++ .../user/model/UserResetPasswordDto.java | 28 + .../java/ru/ulstu/user/model/UserRole.java | 46 ++ .../ulstu/user/model/UserRoleConstants.java | 6 + .../java/ru/ulstu/user/model/UserRoleDto.java | 36 ++ .../java/ru/ulstu/user/model/UserSession.java | 75 +++ .../ulstu/user/model/UserSessionListDto.java | 56 ++ .../ulstu/user/repository/UserRepository.java | 28 + .../user/repository/UserRoleRepository.java | 7 + .../repository/UserSessionRepository.java | 13 + .../ulstu/user/scheduler/UserScheduler.java | 50 ++ .../user/scheduler/UserSessionScheduler.java | 37 ++ .../ru/ulstu/user/service/MailService.java | 78 +++ .../ru/ulstu/user/service/UserMapper.java | 66 +++ .../ru/ulstu/user/service/UserService.java | 285 ++++++++++ .../user/service/UserSessionService.java | 57 ++ .../java/ru/ulstu/user/util/UserUtils.java | 35 ++ src/main/resources/application.properties | 36 ++ src/main/resources/commits.log | 489 ++++++++++++++++++ .../db/changelog-20180301_130000-schema.xml | 15 + .../db/changelog-20180301_140000-data.xml | 38 ++ .../db/changelog-20180301_140000-schema.xml | 55 ++ .../db/changelog-20180305_100000-schema.xml | 16 + .../db/changelog-20180321_193000-data.xml | 11 + .../db/changelog-20180405_110000-schema.xml | 43 ++ .../db/changelog-20180428_110000-schema.xml | 15 + src/main/resources/db/changelog-master.xml | 13 + .../mail_templates/activationEmail.html | 25 + .../mail_templates/passwordResetEmail.html | 25 + .../main/resources/public/css}/agency.css | 0 .../main/resources/public/css}/agency.min.css | 0 .../main/resources/public/img}/_header-bg.jpg | Bin .../main/resources/public/img}/about/1.jpg | Bin .../main/resources/public/img}/about/2.jpg | Bin .../main/resources/public/img}/about/3.jpg | Bin .../main/resources/public/img}/about/4.jpg | Bin .../main/resources/public/img}/header-bg.jpg | Bin .../resources/public/img}/main/career.jpg | Bin .../main/resources/public/img}/main/conf.jpg | Bin .../resources/public/img}/main/grants.jpg | Bin .../resources/public/img}/main/papers.jpg | Bin .../resources/public/img}/main/projects.jpg | Bin .../resources/public/img}/main/students.jpg | Bin .../main/resources/public/img}/main/tasks.jpg | Bin .../main/resources/public/img}/main/team.jpg | Bin .../resources/public/img}/main/templates.jpg | Bin .../main/resources/public/img}/map-image.png | Bin .../main/resources/public/js}/agency.js | 0 .../main/resources/public/js}/agency.min.js | 0 .../main/resources/public/js}/contact_me.js | 0 .../resources/public/js}/contact_me.min.js | 0 .../public/js}/jqBootstrapValidation.js | 0 .../public/js}/jqBootstrapValidation.min.js | 0 .../resources/public/mail}/contact_me.php | 0 .../main/resources/public/scss}/_contact.scss | 0 .../main/resources/public/scss}/_footer.scss | 0 .../main/resources/public/scss}/_global.scss | 0 .../resources/public/scss}/_masthead.scss | 0 .../main/resources/public/scss}/_mixins.scss | 0 .../main/resources/public/scss}/_navbar.scss | 0 .../resources/public/scss}/_portfolio.scss | 0 .../resources/public/scss}/_services.scss | 0 .../main/resources/public/scss}/_team.scss | 0 .../resources/public/scss}/_timeline.scss | 0 .../resources/public/scss}/_variables.scss | 0 .../main/resources/public/scss}/agency.scss | 0 .../vendor}/bootstrap/css/bootstrap.css | 0 .../vendor}/bootstrap/css/bootstrap.css.map | 0 .../vendor}/bootstrap/css/bootstrap.min.css | 0 .../bootstrap/css/bootstrap.min.css.map | 0 .../vendor}/bootstrap/js/bootstrap.bundle.js | 0 .../bootstrap/js/bootstrap.bundle.js.map | 0 .../bootstrap/js/bootstrap.bundle.min.js | 0 .../bootstrap/js/bootstrap.bundle.min.js.map | 0 .../public/vendor}/bootstrap/js/bootstrap.js | 0 .../vendor}/bootstrap/js/bootstrap.js.map | 0 .../vendor}/bootstrap/js/bootstrap.min.js | 0 .../vendor}/bootstrap/js/bootstrap.min.js.map | 0 .../vendor}/font-awesome/css/font-awesome.css | 0 .../font-awesome/css/font-awesome.css.map | 0 .../font-awesome/css/font-awesome.min.css | 0 .../font-awesome/fonts/FontAwesome.otf | Bin .../fonts/fontawesome-webfont.eot | Bin .../fonts/fontawesome-webfont.svg | 0 .../fonts/fontawesome-webfont.ttf | Bin .../fonts/fontawesome-webfont.woff | Bin .../fonts/fontawesome-webfont.woff2 | Bin .../vendor}/font-awesome/less/animated.less | 0 .../font-awesome/less/bordered-pulled.less | 0 .../vendor}/font-awesome/less/core.less | 0 .../font-awesome/less/fixed-width.less | 0 .../font-awesome/less/font-awesome.less | 0 .../vendor}/font-awesome/less/icons.less | 0 .../vendor}/font-awesome/less/larger.less | 0 .../vendor}/font-awesome/less/list.less | 0 .../vendor}/font-awesome/less/mixins.less | 0 .../vendor}/font-awesome/less/path.less | 0 .../font-awesome/less/rotated-flipped.less | 0 .../font-awesome/less/screen-reader.less | 0 .../vendor}/font-awesome/less/stacked.less | 0 .../vendor}/font-awesome/less/variables.less | 0 .../vendor}/font-awesome/scss/_animated.scss | 0 .../font-awesome/scss/_bordered-pulled.scss | 0 .../vendor}/font-awesome/scss/_core.scss | 0 .../font-awesome/scss/_fixed-width.scss | 0 .../vendor}/font-awesome/scss/_icons.scss | 0 .../vendor}/font-awesome/scss/_larger.scss | 0 .../vendor}/font-awesome/scss/_list.scss | 0 .../vendor}/font-awesome/scss/_mixins.scss | 0 .../vendor}/font-awesome/scss/_path.scss | 0 .../font-awesome/scss/_rotated-flipped.scss | 0 .../font-awesome/scss/_screen-reader.scss | 0 .../vendor}/font-awesome/scss/_stacked.scss | 0 .../vendor}/font-awesome/scss/_variables.scss | 0 .../font-awesome/scss/font-awesome.scss | 0 .../resources/public/vendor}/google/droid.css | 0 .../public/vendor}/google/kaushan.css | 0 .../public/vendor}/google/montserrat.css | 0 .../public/vendor}/google/roboto.css | 0 .../jquery.easing.compatibility.js | 0 .../vendor}/jquery-easing/jquery.easing.js | 0 .../jquery-easing/jquery.easing.min.js | 0 .../resources/public/vendor}/jquery/jquery.js | 0 .../public/vendor}/jquery/jquery.min.js | 0 .../public/vendor}/jquery/jquery.min.map | 0 .../public/vendor}/jquery/jquery.slim.js | 0 .../public/vendor}/jquery/jquery.slim.min.js | 0 .../public/vendor}/jquery/jquery.slim.min.map | 0 src/main/resources/sample.jks | Bin 0 -> 2264 bytes src/main/resources/static/favicon.ico | Bin 0 -> 2166 bytes src/main/resources/static/favicon_big.ico | Bin 0 -> 4614 bytes src/main/resources/templates/activate.html | 40 ++ .../resources/templates/admin/commits.html | 24 + .../resources/templates/admin/userList.html | 24 + .../templates/admin/userSessions.html | 24 + src/main/resources/templates/default.html | 61 +++ src/main/resources/templates/error/403.html | 13 + src/main/resources/templates/error/404.html | 13 + src/main/resources/templates/error/500.html | 13 + src/main/resources/templates/home.html | 14 + .../main/resources/templates/index.html | 79 +-- src/main/resources/templates/login.html | 117 +++++ .../main/resources/templates/paper.html | 0 src/main/resources/templates/papers.html | 63 +++ src/main/resources/templates/reset.html | 77 +++ .../resources/templates/resetRequest.html | 57 ++ src/test/java/BalanceEmployeeTest.java | 88 ++++ src/test/java/XlsDocumentBuilderTest.java | 126 +++++ 229 files changed, 5885 insertions(+), 175 deletions(-) create mode 100644 build.gradle create mode 100644 checkstyle.xml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat delete mode 100644 papers.html create mode 100644 src/main/java/ru/ulstu/NgTrackerApplication.java create mode 100644 src/main/java/ru/ulstu/commit/controller/CommitController.java create mode 100644 src/main/java/ru/ulstu/commit/model/CommitListDto.java create mode 100644 src/main/java/ru/ulstu/commit/service/CommitService.java create mode 100644 src/main/java/ru/ulstu/configuration/ApplicationProperties.java create mode 100644 src/main/java/ru/ulstu/configuration/AsyncConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/Constants.java create mode 100644 src/main/java/ru/ulstu/configuration/ControllersConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/HttpListenerConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/JacksonConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/MailTemplateConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/MvcConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/SecurityConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/SwaggerConfiguration.java create mode 100644 src/main/java/ru/ulstu/core/controller/AdviceController.java create mode 100644 src/main/java/ru/ulstu/core/error/EntityIdIsNullException.java create mode 100644 src/main/java/ru/ulstu/core/error/OdinException.java create mode 100644 src/main/java/ru/ulstu/core/error/XlsLoadException.java create mode 100644 src/main/java/ru/ulstu/core/error/XlsParseException.java create mode 100644 src/main/java/ru/ulstu/core/jpa/OffsetablePageRequest.java create mode 100644 src/main/java/ru/ulstu/core/model/BaseEntity.java create mode 100644 src/main/java/ru/ulstu/core/model/ErrorConstants.java create mode 100644 src/main/java/ru/ulstu/core/model/TreeDto.java create mode 100644 src/main/java/ru/ulstu/core/model/TreeEntity.java create mode 100644 src/main/java/ru/ulstu/core/model/response/ControllerResponse.java create mode 100644 src/main/java/ru/ulstu/core/model/response/ControllerResponseError.java create mode 100644 src/main/java/ru/ulstu/core/model/response/PageableItems.java create mode 100644 src/main/java/ru/ulstu/core/model/response/Response.java create mode 100644 src/main/java/ru/ulstu/core/model/response/ResponseExtended.java create mode 100644 src/main/java/ru/ulstu/core/repository/JpaDetachableRepository.java create mode 100644 src/main/java/ru/ulstu/core/repository/JpaDetachableRepositoryImpl.java create mode 100644 src/main/java/ru/ulstu/core/service/TreeService.java create mode 100644 src/main/java/ru/ulstu/core/service/XlsDocumentBuilder.java create mode 100644 src/main/java/ru/ulstu/core/util/DateUtils.java create mode 100644 src/main/java/ru/ulstu/core/util/NumberUtils.java create mode 100644 src/main/java/ru/ulstu/core/util/StreamApiUtils.java create mode 100644 src/main/java/ru/ulstu/odin/controller/OdinController.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinBooleanField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinCollectionField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinDateField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinDto.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinMetadata.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinNumericField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinObjectField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinStringField.java create mode 100644 src/main/java/ru/ulstu/odin/model/OdinVoid.java create mode 100644 src/main/java/ru/ulstu/odin/model/annotation/OdinCaption.java create mode 100644 src/main/java/ru/ulstu/odin/model/annotation/OdinDate.java create mode 100644 src/main/java/ru/ulstu/odin/model/annotation/OdinNumeric.java create mode 100644 src/main/java/ru/ulstu/odin/model/annotation/OdinReadOnly.java create mode 100644 src/main/java/ru/ulstu/odin/model/annotation/OdinString.java create mode 100644 src/main/java/ru/ulstu/odin/model/annotation/OdinVisible.java create mode 100644 src/main/java/ru/ulstu/odin/service/OdinService.java create mode 100644 src/main/java/ru/ulstu/odinexample/controller/OdinExampleController.java create mode 100644 src/main/java/ru/ulstu/odinexample/model/OdinExampleDto.java create mode 100644 src/main/java/ru/ulstu/odinexample/model/OdinExampleListDto.java create mode 100644 src/main/java/ru/ulstu/odinexample/service/OdinExampleService.java create mode 100644 src/main/java/ru/ulstu/user/component/IpAddressResolver.java create mode 100644 src/main/java/ru/ulstu/user/component/UserSessionLoginHandler.java create mode 100644 src/main/java/ru/ulstu/user/component/UserSessionLogoutHandler.java create mode 100644 src/main/java/ru/ulstu/user/controller/UserController.java create mode 100644 src/main/java/ru/ulstu/user/error/UserActivationError.java create mode 100644 src/main/java/ru/ulstu/user/error/UserEmailExistsException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserIdExistsException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserIsUndeadException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserLoginExistsException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserNotActivatedException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserNotFoundException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserPasswordsNotValidOrNotMatchException.java create mode 100644 src/main/java/ru/ulstu/user/error/UserResetKeyError.java create mode 100644 src/main/java/ru/ulstu/user/model/User.java create mode 100644 src/main/java/ru/ulstu/user/model/UserDto.java create mode 100644 src/main/java/ru/ulstu/user/model/UserListDto.java create mode 100644 src/main/java/ru/ulstu/user/model/UserResetPasswordDto.java create mode 100644 src/main/java/ru/ulstu/user/model/UserRole.java create mode 100644 src/main/java/ru/ulstu/user/model/UserRoleConstants.java create mode 100644 src/main/java/ru/ulstu/user/model/UserRoleDto.java create mode 100644 src/main/java/ru/ulstu/user/model/UserSession.java create mode 100644 src/main/java/ru/ulstu/user/model/UserSessionListDto.java create mode 100644 src/main/java/ru/ulstu/user/repository/UserRepository.java create mode 100644 src/main/java/ru/ulstu/user/repository/UserRoleRepository.java create mode 100644 src/main/java/ru/ulstu/user/repository/UserSessionRepository.java create mode 100644 src/main/java/ru/ulstu/user/scheduler/UserScheduler.java create mode 100644 src/main/java/ru/ulstu/user/scheduler/UserSessionScheduler.java create mode 100644 src/main/java/ru/ulstu/user/service/MailService.java create mode 100644 src/main/java/ru/ulstu/user/service/UserMapper.java create mode 100644 src/main/java/ru/ulstu/user/service/UserService.java create mode 100644 src/main/java/ru/ulstu/user/service/UserSessionService.java create mode 100644 src/main/java/ru/ulstu/user/util/UserUtils.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/commits.log create mode 100644 src/main/resources/db/changelog-20180301_130000-schema.xml create mode 100644 src/main/resources/db/changelog-20180301_140000-data.xml create mode 100644 src/main/resources/db/changelog-20180301_140000-schema.xml create mode 100644 src/main/resources/db/changelog-20180305_100000-schema.xml create mode 100644 src/main/resources/db/changelog-20180321_193000-data.xml create mode 100644 src/main/resources/db/changelog-20180405_110000-schema.xml create mode 100644 src/main/resources/db/changelog-20180428_110000-schema.xml create mode 100644 src/main/resources/db/changelog-master.xml create mode 100644 src/main/resources/mail_templates/activationEmail.html create mode 100644 src/main/resources/mail_templates/passwordResetEmail.html rename {css => src/main/resources/public/css}/agency.css (100%) rename {css => src/main/resources/public/css}/agency.min.css (100%) rename {img => src/main/resources/public/img}/_header-bg.jpg (100%) rename {img => src/main/resources/public/img}/about/1.jpg (100%) rename {img => src/main/resources/public/img}/about/2.jpg (100%) rename {img => src/main/resources/public/img}/about/3.jpg (100%) rename {img => src/main/resources/public/img}/about/4.jpg (100%) rename {img => src/main/resources/public/img}/header-bg.jpg (100%) rename {img => src/main/resources/public/img}/main/career.jpg (100%) rename {img => src/main/resources/public/img}/main/conf.jpg (100%) rename {img => src/main/resources/public/img}/main/grants.jpg (100%) rename {img => src/main/resources/public/img}/main/papers.jpg (100%) rename {img => src/main/resources/public/img}/main/projects.jpg (100%) rename {img => src/main/resources/public/img}/main/students.jpg (100%) rename {img => src/main/resources/public/img}/main/tasks.jpg (100%) rename {img => src/main/resources/public/img}/main/team.jpg (100%) rename {img => src/main/resources/public/img}/main/templates.jpg (100%) rename {img => src/main/resources/public/img}/map-image.png (100%) rename {js => src/main/resources/public/js}/agency.js (100%) rename {js => src/main/resources/public/js}/agency.min.js (100%) rename {js => src/main/resources/public/js}/contact_me.js (100%) rename {js => src/main/resources/public/js}/contact_me.min.js (100%) rename {js => src/main/resources/public/js}/jqBootstrapValidation.js (100%) rename {js => src/main/resources/public/js}/jqBootstrapValidation.min.js (100%) rename {mail => src/main/resources/public/mail}/contact_me.php (100%) rename {scss => src/main/resources/public/scss}/_contact.scss (100%) rename {scss => src/main/resources/public/scss}/_footer.scss (100%) rename {scss => src/main/resources/public/scss}/_global.scss (100%) rename {scss => src/main/resources/public/scss}/_masthead.scss (100%) rename {scss => src/main/resources/public/scss}/_mixins.scss (100%) rename {scss => src/main/resources/public/scss}/_navbar.scss (100%) rename {scss => src/main/resources/public/scss}/_portfolio.scss (100%) rename {scss => src/main/resources/public/scss}/_services.scss (100%) rename {scss => src/main/resources/public/scss}/_team.scss (100%) rename {scss => src/main/resources/public/scss}/_timeline.scss (100%) rename {scss => src/main/resources/public/scss}/_variables.scss (100%) rename {scss => src/main/resources/public/scss}/agency.scss (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/css/bootstrap.css (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/css/bootstrap.css.map (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/css/bootstrap.min.css (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/css/bootstrap.min.css.map (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.bundle.js (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.bundle.js.map (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.bundle.min.js (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.bundle.min.js.map (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.js (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.js.map (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.min.js (100%) rename {vendor => src/main/resources/public/vendor}/bootstrap/js/bootstrap.min.js.map (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/css/font-awesome.css (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/css/font-awesome.css.map (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/css/font-awesome.min.css (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/fonts/FontAwesome.otf (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/fonts/fontawesome-webfont.eot (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/fonts/fontawesome-webfont.svg (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/fonts/fontawesome-webfont.ttf (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/fonts/fontawesome-webfont.woff (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/fonts/fontawesome-webfont.woff2 (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/animated.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/bordered-pulled.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/core.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/fixed-width.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/font-awesome.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/icons.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/larger.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/list.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/mixins.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/path.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/rotated-flipped.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/screen-reader.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/stacked.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/less/variables.less (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_animated.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_bordered-pulled.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_core.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_fixed-width.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_icons.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_larger.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_list.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_mixins.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_path.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_rotated-flipped.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_screen-reader.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_stacked.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/_variables.scss (100%) rename {vendor => src/main/resources/public/vendor}/font-awesome/scss/font-awesome.scss (100%) rename {vendor => src/main/resources/public/vendor}/google/droid.css (100%) rename {vendor => src/main/resources/public/vendor}/google/kaushan.css (100%) rename {vendor => src/main/resources/public/vendor}/google/montserrat.css (100%) rename {vendor => src/main/resources/public/vendor}/google/roboto.css (100%) rename {vendor => src/main/resources/public/vendor}/jquery-easing/jquery.easing.compatibility.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery-easing/jquery.easing.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery-easing/jquery.easing.min.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery/jquery.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery/jquery.min.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery/jquery.min.map (100%) rename {vendor => src/main/resources/public/vendor}/jquery/jquery.slim.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery/jquery.slim.min.js (100%) rename {vendor => src/main/resources/public/vendor}/jquery/jquery.slim.min.map (100%) create mode 100644 src/main/resources/sample.jks create mode 100644 src/main/resources/static/favicon.ico create mode 100644 src/main/resources/static/favicon_big.ico create mode 100644 src/main/resources/templates/activate.html create mode 100644 src/main/resources/templates/admin/commits.html create mode 100644 src/main/resources/templates/admin/userList.html create mode 100644 src/main/resources/templates/admin/userSessions.html create mode 100644 src/main/resources/templates/default.html create mode 100644 src/main/resources/templates/error/403.html create mode 100644 src/main/resources/templates/error/404.html create mode 100644 src/main/resources/templates/error/500.html create mode 100644 src/main/resources/templates/home.html rename index.html => src/main/resources/templates/index.html (72%) create mode 100644 src/main/resources/templates/login.html rename paper.html => src/main/resources/templates/paper.html (100%) create mode 100644 src/main/resources/templates/papers.html create mode 100644 src/main/resources/templates/reset.html create mode 100644 src/main/resources/templates/resetRequest.html create mode 100644 src/test/java/BalanceEmployeeTest.java create mode 100644 src/test/java/XlsDocumentBuilderTest.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..13e3454 --- /dev/null +++ b/build.gradle @@ -0,0 +1,127 @@ +buildscript { + ext { + versionSpringBoot = '1.5.10.RELEASE' + } + + repositories { + mavenLocal() + mavenCentral() + } + + dependencies { + classpath group: 'org.springframework.boot', name: 'spring-boot-gradle-plugin', version: versionSpringBoot + } +} + +group 'ru.ulstu' +version '0.1.0-SNAPSHOT' + +apply plugin: 'java' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' +apply plugin: 'checkstyle' + +build.dependsOn checkstyleMain +bootRun.dependsOn checkstyleMain + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +checkstyle { + + project.ext.checkstyleVersion = '8.8' + project.ext.sevntuChecksVersion = '1.28.0' + + repositories { + mavenLocal() + mavenCentral() + } + + ignoreFailures = false + showViolations = true + maxErrors = 1 + maxWarnings = 1 + configFile = file("${project.rootDir}/checkstyle.xml") + + //sourceSets = [sourceSets.main] + //showViolations = true + //reportsDir = file("$project.buildDir/checkstyleReports") + //configProperties = ['baseDir': "$project.projectDir"] + + //https://discuss.gradle.org/t/some-checkstyle-rules-dont-work-in-gradle/16102/4 + checkstyleMain { + source = sourceSets.main.allSource + } + + configurations { + checkstyle + } + + dependencies{ + assert project.hasProperty("checkstyleVersion") + + checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" + checkstyle "com.github.sevntu-checkstyle:sevntu-checks:${sevntuChecksVersion}" + } +} + +task health(dependsOn: [ + 'checkstyleMain' +]) + + +jar { + baseName = 'ng-tracker' +} + +compileJava { + options.encoding = "UTF-8" +} + +repositories { + mavenLocal() + mavenCentral() +} + +configurations { + compile.exclude module: "spring-boot-starter-tomcat" + compile.exclude module: "bcmail-jdk14" + compile.exclude module: "bcprov-jdk14" + compile.exclude module: "bctsp-jdk14" +} + +dependencies { + compile group: 'org.springframework.boot', name: 'spring-boot-starter-web' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-security' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-mail' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-jetty' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf' + compile group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity4' + compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner' + compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-hibernate5' + + compile group: 'postgresql', name: 'postgresql', version: '9.1-901.jdbc4' + + compile group: 'org.liquibase', name: 'liquibase-core', version: '3.5.3' + compile group: 'com.mattbertolini', name: 'liquibase-slf4j', version: '2.0.0' + + compile group: 'org.apache.poi', name: 'poi', version: '3.9' + compile group: 'org.apache.poi', name: 'poi-ooxml', version: '3.9' + + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.7' + + compile group: 'com.lowagie', name: 'itext', version: '2.1.7' + + compile group: 'org.webjars', name: 'bootstrap', version: '3.3.7-1' + compile group: 'org.webjars', name: 'bootstrap-select', version: '1.12.4' + compile group: 'org.webjars', name: 'jquery', version: '3.3.1-1' + compile group: 'org.webjars', name: 'font-awesome', version: '4.7.0' + compile group: 'org.webjars', name: 'jstree', version: '3.3.3' + + compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.5.0' + compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.5.0' + + testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' +} \ No newline at end of file diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..5877ec9 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..59c3905659d674fb3021e1465ad97d87f2b7221a GIT binary patch literal 54706 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..98bcec3 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Feb 06 11:48:53 SAMT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9aa616c --- /dev/null +++ b/gradlew @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/papers.html b/papers.html deleted file mode 100644 index 6a67c4e..0000000 --- a/papers.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - NG-Tacker - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - - - - - - - diff --git a/src/main/java/ru/ulstu/NgTrackerApplication.java b/src/main/java/ru/ulstu/NgTrackerApplication.java new file mode 100644 index 0000000..5e6ee4f --- /dev/null +++ b/src/main/java/ru/ulstu/NgTrackerApplication.java @@ -0,0 +1,14 @@ +package ru.ulstu; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import ru.ulstu.core.repository.JpaDetachableRepositoryImpl; + +@SpringBootApplication +@EnableJpaRepositories(repositoryBaseClass = JpaDetachableRepositoryImpl.class) +public class NgTrackerApplication { + public static void main(String[] args) { + SpringApplication.run(NgTrackerApplication.class, args); + } +} diff --git a/src/main/java/ru/ulstu/commit/controller/CommitController.java b/src/main/java/ru/ulstu/commit/controller/CommitController.java new file mode 100644 index 0000000..44f99cb --- /dev/null +++ b/src/main/java/ru/ulstu/commit/controller/CommitController.java @@ -0,0 +1,33 @@ +package ru.ulstu.commit.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.ulstu.configuration.Constants; +import ru.ulstu.core.model.response.PageableItems; +import ru.ulstu.commit.model.CommitListDto; +import ru.ulstu.commit.service.CommitService; +import ru.ulstu.core.model.response.Response; +import ru.ulstu.odin.controller.OdinController; +import ru.ulstu.odin.model.OdinVoid; + +import static ru.ulstu.commit.controller.CommitController.URL; + +@RestController +@RequestMapping(URL) +public class CommitController extends OdinController { + public static final String URL = Constants.API_1_0 + "commits"; + private final CommitService commitService; + + public CommitController(CommitService commitService) { + super(CommitListDto.class); + this.commitService = commitService; + } + + @GetMapping("") + public Response> getCommits(@RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "count", defaultValue = "10") int count) { + return new Response<>(commitService.getCommits(offset, count)); + } +} diff --git a/src/main/java/ru/ulstu/commit/model/CommitListDto.java b/src/main/java/ru/ulstu/commit/model/CommitListDto.java new file mode 100644 index 0000000..4650484 --- /dev/null +++ b/src/main/java/ru/ulstu/commit/model/CommitListDto.java @@ -0,0 +1,53 @@ +package ru.ulstu.commit.model; + +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.odin.model.annotation.OdinDate; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class CommitListDto { + private final static String DELIMITER = ";"; + @OdinCaption("Дата") + @OdinDate(type = OdinDate.OdinDateType.DATETIME) + private final Date date; + @OdinCaption("Пользователь") + private final String userName; + @OdinCaption("Сообщение") + private final String message; + + public CommitListDto(String data) { + List datas = Arrays.stream(data.split(DELIMITER)) + .filter(d -> d != null && !d.isEmpty()) + .collect(Collectors.toList()); + if (datas.size() > 2) { + this.userName = datas.get(0); + this.message = datas.get(2); + SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd hh:mm:ss yyyy Z", Locale.US); + try { + this.date = format.parse(datas.get(1)); + } catch (ParseException e) { + throw new RuntimeException("wrong commits date"); + } + return; + } + throw new RuntimeException("wrong commits data"); + } + + public String getMessage() { + return message; + } + + public Date getDate() { + return date; + } + + public String getUserName() { + return userName; + } +} diff --git a/src/main/java/ru/ulstu/commit/service/CommitService.java b/src/main/java/ru/ulstu/commit/service/CommitService.java new file mode 100644 index 0000000..f77d3ac --- /dev/null +++ b/src/main/java/ru/ulstu/commit/service/CommitService.java @@ -0,0 +1,62 @@ +package ru.ulstu.commit.service; + +import org.springframework.stereotype.Service; +import ru.ulstu.commit.model.CommitListDto; +import ru.ulstu.core.model.response.PageableItems; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +public class CommitService { + private static final String COMMITS_FILE_NAME = "/commits.log"; + + private final List commits; + + public CommitService() { + commits = getCommits(); + } + + public PageableItems getCommits(int offset, int count) { + return new PageableItems<>(commits.size(), commits.stream() + .skip(offset) + .limit(count) + .collect(Collectors.toList())); + } + + private List getCommits() { + return getFileContent() + .stream() + .map(CommitListDto::new) + .filter(Objects::nonNull) + .sorted((c1, c2) -> c2.getDate().compareTo(c1.getDate())) + .collect(Collectors.toList()); + } + + private List getFileContent() { + + List result = new ArrayList<>(); + + //Get file from resources folder + InputStream is = getClass().getResourceAsStream(COMMITS_FILE_NAME); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + try { + while ((line = reader.readLine()) != null) { + result.add(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + Collections.reverse(result); + return result; + } +} + diff --git a/src/main/java/ru/ulstu/configuration/ApplicationProperties.java b/src/main/java/ru/ulstu/configuration/ApplicationProperties.java new file mode 100644 index 0000000..b89641a --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/ApplicationProperties.java @@ -0,0 +1,41 @@ +package ru.ulstu.configuration; + +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Component +@ConfigurationProperties(prefix = "ng-tracker") +@Validated +public class ApplicationProperties { + @NotBlank + private String baseUrl; + @NotBlank + private String undeadUserLogin; + private boolean devMode; + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getUndeadUserLogin() { + return undeadUserLogin; + } + + public void setUndeadUserLogin(String undeadUserLogin) { + this.undeadUserLogin = undeadUserLogin; + } + + public boolean isDevMode() { + return devMode; + } + + public void setDevMode(boolean devMode) { + this.devMode = devMode; + } +} diff --git a/src/main/java/ru/ulstu/configuration/AsyncConfiguration.java b/src/main/java/ru/ulstu/configuration/AsyncConfiguration.java new file mode 100644 index 0000000..bdf0613 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/AsyncConfiguration.java @@ -0,0 +1,39 @@ +package ru.ulstu.configuration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +@EnableScheduling +public class AsyncConfiguration implements AsyncConfigurer { + private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class); + + @Override + @Bean(name = "taskExecutor") + public Executor getAsyncExecutor() { + log.debug("Creating Async Task Executor"); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(2); + executor.setQueueCapacity(500); + executor.setThreadNamePrefix("balance-executor-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } +} diff --git a/src/main/java/ru/ulstu/configuration/Constants.java b/src/main/java/ru/ulstu/configuration/Constants.java new file mode 100644 index 0000000..0a2268a --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/Constants.java @@ -0,0 +1,20 @@ +package ru.ulstu.configuration; + +public class Constants { + public static final String API_1_0 = "/api/1.0/"; + + public static final String MAIL_ACTIVATE = "Account activation"; + public static final String MAIL_RESET = "Password reset"; + public static final int MIN_PASSWORD_LENGTH = 6; + + public static final String LOGIN_REGEX = "^[_'.@A-Za-z0-9-]*$"; + + public static final String COOKIES_NAME = "JSESSIONID"; + public static final String LOGOUT_URL = "/login?logout"; + public static final String SESSION_ID_ATTR = "sessionId"; + public static final int SESSION_TIMEOUT_SECONDS = 30 * 60; + + public static final String PASSWORD_RESET_REQUEST_PAGE = "/resetRequest"; + public static final String PASSWORD_RESET_PAGE = "/reset"; + +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/configuration/ControllersConfiguration.java b/src/main/java/ru/ulstu/configuration/ControllersConfiguration.java new file mode 100644 index 0000000..2d92313 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/ControllersConfiguration.java @@ -0,0 +1,24 @@ +package ru.ulstu.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +class ControllersConfiguration { + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("GET"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/src/main/java/ru/ulstu/configuration/HttpListenerConfiguration.java b/src/main/java/ru/ulstu/configuration/HttpListenerConfiguration.java new file mode 100644 index 0000000..bcbcab8 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/HttpListenerConfiguration.java @@ -0,0 +1,30 @@ +package ru.ulstu.configuration; + +import org.eclipse.jetty.server.ServerConnector; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; +import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; +import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.jetty.JettyServerCustomizer; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HttpListenerConfiguration implements EmbeddedServletContainerCustomizer { + @Value("${server.http.port}") + private int httpPort; + + private void configureJetty(JettyEmbeddedServletContainerFactory jettyFactory) { + jettyFactory.addServerCustomizers((JettyServerCustomizer) server -> { + ServerConnector serverConnector = new ServerConnector(server); + serverConnector.setPort(httpPort); + server.addConnector(serverConnector); + }); + } + + @Override + public void customize(ConfigurableEmbeddedServletContainer container) { + if (container instanceof JettyEmbeddedServletContainerFactory) { + configureJetty((JettyEmbeddedServletContainerFactory) container); + } + } +} diff --git a/src/main/java/ru/ulstu/configuration/JacksonConfiguration.java b/src/main/java/ru/ulstu/configuration/JacksonConfiguration.java new file mode 100644 index 0000000..108a28f --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/JacksonConfiguration.java @@ -0,0 +1,20 @@ +package ru.ulstu.configuration; + +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfiguration { + + @Bean + public Hibernate5Module hibernate5Module() { + return new Hibernate5Module(); + } + + @Bean + public AfterburnerModule afterburnerModule() { + return new AfterburnerModule(); + } +} diff --git a/src/main/java/ru/ulstu/configuration/MailTemplateConfiguration.java b/src/main/java/ru/ulstu/configuration/MailTemplateConfiguration.java new file mode 100644 index 0000000..68c201b --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/MailTemplateConfiguration.java @@ -0,0 +1,21 @@ +package ru.ulstu.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +import java.nio.charset.StandardCharsets; + +@Configuration +public class MailTemplateConfiguration { + @Bean + public ClassLoaderTemplateResolver emailTemplateResolver() { + ClassLoaderTemplateResolver emailTemplateResolver = new ClassLoaderTemplateResolver(); + emailTemplateResolver.setPrefix("mail_templates/"); + emailTemplateResolver.setSuffix(".html"); + emailTemplateResolver.setTemplateMode("HTML5"); + emailTemplateResolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); + emailTemplateResolver.setOrder(1); + return emailTemplateResolver; + } +} diff --git a/src/main/java/ru/ulstu/configuration/MvcConfiguration.java b/src/main/java/ru/ulstu/configuration/MvcConfiguration.java new file mode 100644 index 0000000..bbe0885 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/MvcConfiguration.java @@ -0,0 +1,24 @@ +package ru.ulstu.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +@Configuration +public class MvcConfiguration extends WebMvcConfigurerAdapter { + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/{articlename:\\w+}"); + //registry.addViewController("/admin/{articlename:\\w+}"); + registry.addRedirectViewController("/", "/index"); + registry.addRedirectViewController("/default", "/index"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry + .addResourceHandler("/webjars/**") + .addResourceLocations("/webjars/"); + } +} diff --git a/src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..19370da --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java @@ -0,0 +1,13 @@ +package ru.ulstu.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/ru/ulstu/configuration/SecurityConfiguration.java b/src/main/java/ru/ulstu/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..c0f34d6 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/SecurityConfiguration.java @@ -0,0 +1,118 @@ +package ru.ulstu.configuration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import ru.ulstu.user.controller.UserController; +import ru.ulstu.user.model.UserRoleConstants; +import ru.ulstu.user.service.UserService; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + + @Value("${server.http.port}") + private int httpPort; + @Value("${server.port}") + private int httpsPort; + + private final UserService userService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final AuthenticationSuccessHandler authenticationSuccessHandler; + private final LogoutSuccessHandler logoutSuccessHandler; + private final ApplicationProperties applicationProperties; + + public SecurityConfiguration(UserService userService, + BCryptPasswordEncoder bCryptPasswordEncoder, + AuthenticationSuccessHandler authenticationSuccessHandler, + LogoutSuccessHandler logoutSuccessHandler, + ApplicationProperties applicationProperties) { + this.userService = userService; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + this.authenticationSuccessHandler = authenticationSuccessHandler; + this.logoutSuccessHandler = logoutSuccessHandler; + this.applicationProperties = applicationProperties; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf() + .disable(); + if (applicationProperties.isDevMode()) { + log.debug("Security disabled"); + http.authorizeRequests() + .anyRequest() + .permitAll(); + http.anonymous() + .principal("developer") + .authorities(UserRoleConstants.ADMIN); + } else { + log.debug("Security enabled"); + http.authorizeRequests() + .antMatchers(UserController.ACTIVATE_URL).permitAll() + .antMatchers(Constants.PASSWORD_RESET_REQUEST_PAGE).permitAll() + .antMatchers(Constants.PASSWORD_RESET_PAGE).permitAll() + .antMatchers(UserController.URL + UserController.REGISTER_URL).permitAll() + .antMatchers(UserController.URL + UserController.ACTIVATE_URL).permitAll() + .antMatchers(UserController.URL + UserController.PASSWORD_RESET_REQUEST_URL).permitAll() + .antMatchers(UserController.URL + UserController.PASSWORD_RESET_URL).permitAll() + .antMatchers("/swagger-ui.html").hasAuthority(UserRoleConstants.ADMIN) + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/login") + .successHandler(authenticationSuccessHandler) + .permitAll() + .and() + .logout() + .logoutSuccessHandler(logoutSuccessHandler) + .logoutSuccessUrl(Constants.LOGOUT_URL) + .invalidateHttpSession(false) + .clearAuthentication(true) + .deleteCookies(Constants.COOKIES_NAME) + .permitAll(); + } + http.portMapper() + .http(httpPort) + .mapsTo(httpsPort) + .and() + .requiresChannel() + .anyRequest() + .requiresSecure(); + } + + @Override + public void configure(WebSecurity web) { + web.ignoring() + .antMatchers("/css/**") + .antMatchers("/js/**") + .antMatchers("/templates/**") + .antMatchers("/webjars/**"); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) { + if (applicationProperties.isDevMode()) { + return; + } + try { + auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder); + } catch (Exception e) { + throw new BeanInitializationException("Security configuration failed", e); + } + } +} diff --git a/src/main/java/ru/ulstu/configuration/SwaggerConfiguration.java b/src/main/java/ru/ulstu/configuration/SwaggerConfiguration.java new file mode 100644 index 0000000..c51811e --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/SwaggerConfiguration.java @@ -0,0 +1,23 @@ +package ru.ulstu.configuration; + +import com.google.common.base.Predicates; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SwaggerConfiguration { + @Bean + public Docket swaggerApi() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(Predicates.not(PathSelectors.regex("/error"))) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/core/controller/AdviceController.java b/src/main/java/ru/ulstu/core/controller/AdviceController.java new file mode 100644 index 0000000..916359a --- /dev/null +++ b/src/main/java/ru/ulstu/core/controller/AdviceController.java @@ -0,0 +1,99 @@ +package ru.ulstu.core.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import ru.ulstu.core.error.EntityIdIsNullException; +import ru.ulstu.core.model.ErrorConstants; +import ru.ulstu.core.model.response.Response; +import ru.ulstu.core.model.response.ResponseExtended; +import ru.ulstu.user.error.*; +import ru.ulstu.user.error.*; + +import java.util.Set; +import java.util.stream.Collectors; + +@RestController +@ControllerAdvice +public class AdviceController { + private final Logger log = LoggerFactory.getLogger(AdviceController.class); + + private Response handleException(ErrorConstants error) { + log.warn(error.toString()); + return new Response<>(error); + } + + private ResponseExtended handleException(ErrorConstants error, E errorData) { + log.warn(error.toString()); + return new ResponseExtended<>(error, errorData); + } + + @ExceptionHandler(EntityIdIsNullException.class) + public Response handleEntityIdIsNullException(Throwable e) { + return handleException(ErrorConstants.ID_IS_NULL); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseExtended> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + final Set errors = e.getBindingResult().getAllErrors().stream() + .filter(error -> error instanceof FieldError) + .map(error -> ((FieldError) error).getField()) + .collect(Collectors.toSet()); + return handleException(ErrorConstants.VALIDATION_ERROR, errors); + } + + @ExceptionHandler(UserIdExistsException.class) + public Response handleUserIdExistsException(Throwable e) { + return handleException(ErrorConstants.USER_ID_EXISTS); + } + + @ExceptionHandler(UserActivationError.class) + public ResponseExtended handleUserActivationError(Throwable e) { + return handleException(ErrorConstants.USER_ACTIVATION_ERROR, e.getMessage()); + } + + @ExceptionHandler(UserLoginExistsException.class) + public ResponseExtended handleUserLoginExistsException(Throwable e) { + return handleException(ErrorConstants.USER_LOGIN_EXISTS, e.getMessage()); + } + + @ExceptionHandler(UserEmailExistsException.class) + public ResponseExtended handleUserEmailExistsException(Throwable e) { + return handleException(ErrorConstants.USER_EMAIL_EXISTS, e.getMessage()); + } + + @ExceptionHandler(UserPasswordsNotValidOrNotMatchException.class) + public Response handleUserPasswordsNotValidOrNotMatchException(Throwable e) { + return handleException(ErrorConstants.USER_PASSWORDS_NOT_VALID_OR_NOT_MATCH); + } + + @ExceptionHandler(UserNotFoundException.class) + public ResponseExtended handleUserNotFoundException(Throwable e) { + return handleException(ErrorConstants.USER_NOT_FOUND, e.getMessage()); + } + + @ExceptionHandler(UserNotActivatedException.class) + public Response handleUserNotActivatedException(Throwable e) { + return handleException(ErrorConstants.USER_NOT_ACTIVATED); + } + + @ExceptionHandler(UserResetKeyError.class) + public ResponseExtended handleUserResetKeyError(Throwable e) { + return handleException(ErrorConstants.USER_RESET_ERROR, e.getMessage()); + } + + @ExceptionHandler(UserIsUndeadException.class) + public ResponseExtended handleUserIsUndeadException(Throwable e) { + return handleException(ErrorConstants.USER_UNDEAD_ERROR, e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseExtended handleUnknownException(Throwable e) { + e.printStackTrace(); + return handleException(ErrorConstants.UNKNOWN, e.getMessage()); + } +} diff --git a/src/main/java/ru/ulstu/core/error/EntityIdIsNullException.java b/src/main/java/ru/ulstu/core/error/EntityIdIsNullException.java new file mode 100644 index 0000000..1c95a28 --- /dev/null +++ b/src/main/java/ru/ulstu/core/error/EntityIdIsNullException.java @@ -0,0 +1,6 @@ +package ru.ulstu.core.error; + +public class EntityIdIsNullException extends RuntimeException { + public EntityIdIsNullException() { + } +} diff --git a/src/main/java/ru/ulstu/core/error/OdinException.java b/src/main/java/ru/ulstu/core/error/OdinException.java new file mode 100644 index 0000000..a7513fa --- /dev/null +++ b/src/main/java/ru/ulstu/core/error/OdinException.java @@ -0,0 +1,7 @@ +package ru.ulstu.core.error; + +public class OdinException extends RuntimeException { + public OdinException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/core/error/XlsLoadException.java b/src/main/java/ru/ulstu/core/error/XlsLoadException.java new file mode 100644 index 0000000..372731f --- /dev/null +++ b/src/main/java/ru/ulstu/core/error/XlsLoadException.java @@ -0,0 +1,7 @@ +package ru.ulstu.core.error; + +public class XlsLoadException extends Exception { + public XlsLoadException(String s) { + super(s); + } +} diff --git a/src/main/java/ru/ulstu/core/error/XlsParseException.java b/src/main/java/ru/ulstu/core/error/XlsParseException.java new file mode 100644 index 0000000..fe8c4b3 --- /dev/null +++ b/src/main/java/ru/ulstu/core/error/XlsParseException.java @@ -0,0 +1,7 @@ +package ru.ulstu.core.error; + +public class XlsParseException extends Exception { + public XlsParseException(String s) { + super(s); + } +} diff --git a/src/main/java/ru/ulstu/core/jpa/OffsetablePageRequest.java b/src/main/java/ru/ulstu/core/jpa/OffsetablePageRequest.java new file mode 100644 index 0000000..388e9a1 --- /dev/null +++ b/src/main/java/ru/ulstu/core/jpa/OffsetablePageRequest.java @@ -0,0 +1,97 @@ +package ru.ulstu.core.jpa; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.io.Serializable; + +public class OffsetablePageRequest implements Pageable, Serializable { + private final int offset; + private final int count; + private final Sort sort; + + public OffsetablePageRequest(int offset, int count) { + this(offset, count, null); + } + + public OffsetablePageRequest(int offset, int count, Sort.Direction direction, String... properties) { + this(offset, count, new Sort(direction, properties)); + } + + public OffsetablePageRequest(int offset, int count, Sort sort) { + if (offset < 0) { + throw new IllegalArgumentException("Offset value must not be less than zero!"); + } + if (count < 1) { + throw new IllegalArgumentException("Count value must not be less than one!"); + } + this.offset = offset; + this.count = count; + this.sort = sort; + } + + @Override + public Sort getSort() { + return sort; + } + + @Override + public int getPageSize() { + return count; + } + + @Override + public int getPageNumber() { + return offset / count; + } + + @Override + public int getOffset() { + return offset; + } + + @Override + public boolean hasPrevious() { + return offset > 0; + } + + @Override + public Pageable next() { + return new OffsetablePageRequest(getOffset() + getPageSize(), getPageSize(), getSort()); + } + + @Override + public Pageable previousOrFirst() { + return hasPrevious() ? previous() : first(); + } + + public Pageable previous() { + return getOffset() == 0 ? this : new OffsetablePageRequest(getOffset() - getPageSize(), getPageSize(), getSort()); + } + + @Override + public Pageable first() { + return new OffsetablePageRequest(0, getPageSize(), getSort()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final OffsetablePageRequest other = (OffsetablePageRequest) obj; + return this.offset == other.offset && this.count == other.count; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + offset; + result = prime * result + count; + return result; + } +} diff --git a/src/main/java/ru/ulstu/core/model/BaseEntity.java b/src/main/java/ru/ulstu/core/model/BaseEntity.java new file mode 100644 index 0000000..bd0f1e4 --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/BaseEntity.java @@ -0,0 +1,82 @@ +package ru.ulstu.core.model; + +import javax.persistence.*; +import java.io.Serializable; + +@MappedSuperclass +public abstract class BaseEntity implements Serializable, Comparable { + @Id + @GeneratedValue(strategy = GenerationType.TABLE) + private Integer id; + + @Version + private Integer version; + + public BaseEntity() { + } + + public BaseEntity(Integer id, Integer version) { + this.id = id; + this.version = version; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!getClass().isAssignableFrom(obj.getClass())) { + return false; + } + BaseEntity other = (BaseEntity) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "id=" + id + + ", version=" + version + + '}'; + } + + @Override + public int compareTo(Object o) { + return id != null ? id.compareTo(((BaseEntity) o).getId()) : -1; + } + + public void reset() { + this.id = null; + this.version = null; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/core/model/ErrorConstants.java b/src/main/java/ru/ulstu/core/model/ErrorConstants.java new file mode 100644 index 0000000..ad69b86 --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/ErrorConstants.java @@ -0,0 +1,38 @@ +package ru.ulstu.core.model; + +public enum ErrorConstants { + UNKNOWN(0, "Unknown error"), + ID_IS_NULL(1, "Id of entity has null value"), + VALIDATION_ERROR(2, "Validation error"), + USER_ID_EXISTS(100, "New user can't have id"), + USER_ACTIVATION_ERROR(101, "Invalid activation key"), + USER_EMAIL_EXISTS(102, "User with same email already exists"), + USER_LOGIN_EXISTS(103, "User with same login already exists"), + USER_PASSWORDS_NOT_VALID_OR_NOT_MATCH(104, "User passwords is not valid or not match"), + USER_NOT_FOUND(105, "User is not found"), + USER_NOT_ACTIVATED(106, "User is not activated"), + USER_RESET_ERROR(107, "Invalid reset key"), + USER_UNDEAD_ERROR(108, "Can't edit/delete that user"), + FILE_UPLOAD_ERROR(110, "File upload error"); + + private int code; + private String message; + + ErrorConstants(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return String.format("%d: %s", code, message); + } +} diff --git a/src/main/java/ru/ulstu/core/model/TreeDto.java b/src/main/java/ru/ulstu/core/model/TreeDto.java new file mode 100644 index 0000000..491553c --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/TreeDto.java @@ -0,0 +1,34 @@ +package ru.ulstu.core.model; + +import java.util.ArrayList; +import java.util.List; + +public class TreeDto { + private Integer id; + private String text; + private List children = new ArrayList<>(); + + public TreeDto() { + } + + public TreeDto(TreeEntity item) { + this.text = item.toString(); + this.id = item.getId(); + } + + public TreeDto(String rootName) { + this.text = rootName; + } + + public String getText() { + return text; + } + + public List getChildren() { + return children; + } + + public Integer getId() { + return id; + } +} diff --git a/src/main/java/ru/ulstu/core/model/TreeEntity.java b/src/main/java/ru/ulstu/core/model/TreeEntity.java new file mode 100644 index 0000000..48f1202 --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/TreeEntity.java @@ -0,0 +1,16 @@ +package ru.ulstu.core.model; + +import java.util.List; + +public interface TreeEntity { + + Integer getId(); + + List getChildren(); + + void setChildren(List children); + + T getParent(); + + void setParent(T parent); +} diff --git a/src/main/java/ru/ulstu/core/model/response/ControllerResponse.java b/src/main/java/ru/ulstu/core/model/response/ControllerResponse.java new file mode 100644 index 0000000..72c5ebf --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/response/ControllerResponse.java @@ -0,0 +1,24 @@ +package ru.ulstu.core.model.response; + +class ControllerResponse { + private D data; + private ControllerResponseError error; + + ControllerResponse(D data) { + this.data = data; + this.error = null; + } + + ControllerResponse(ControllerResponseError error) { + this.data = null; + this.error = error; + } + + public D getData() { + return data; + } + + public ControllerResponseError getError() { + return error; + } +} diff --git a/src/main/java/ru/ulstu/core/model/response/ControllerResponseError.java b/src/main/java/ru/ulstu/core/model/response/ControllerResponseError.java new file mode 100644 index 0000000..a26b088 --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/response/ControllerResponseError.java @@ -0,0 +1,25 @@ +package ru.ulstu.core.model.response; + +import ru.ulstu.core.model.ErrorConstants; + +class ControllerResponseError { + private ErrorConstants description; + private D data; + + ControllerResponseError(ErrorConstants description, D data) { + this.description = description; + this.data = data; + } + + public int getCode() { + return description.getCode(); + } + + public String getMessage() { + return description.getMessage(); + } + + public D getData() { + return data; + } +} diff --git a/src/main/java/ru/ulstu/core/model/response/PageableItems.java b/src/main/java/ru/ulstu/core/model/response/PageableItems.java new file mode 100644 index 0000000..ab3047f --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/response/PageableItems.java @@ -0,0 +1,21 @@ +package ru.ulstu.core.model.response; + +import java.util.Collection; + +public class PageableItems { + private final long count; + private final Collection items; + + public PageableItems(long count, Collection items) { + this.count = count; + this.items = items; + } + + public long getCount() { + return count; + } + + public Collection getItems() { + return items; + } +} diff --git a/src/main/java/ru/ulstu/core/model/response/Response.java b/src/main/java/ru/ulstu/core/model/response/Response.java new file mode 100644 index 0000000..4722010 --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/response/Response.java @@ -0,0 +1,16 @@ +package ru.ulstu.core.model.response; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import ru.ulstu.core.model.ErrorConstants; + +public class Response extends ResponseEntity { + + public Response(D data) { + super(new ControllerResponse(data), HttpStatus.OK); + } + + public Response(ErrorConstants error) { + super(new ControllerResponse(new ControllerResponseError<>(error, null)), HttpStatus.OK); + } +} diff --git a/src/main/java/ru/ulstu/core/model/response/ResponseExtended.java b/src/main/java/ru/ulstu/core/model/response/ResponseExtended.java new file mode 100644 index 0000000..1829622 --- /dev/null +++ b/src/main/java/ru/ulstu/core/model/response/ResponseExtended.java @@ -0,0 +1,12 @@ +package ru.ulstu.core.model.response; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import ru.ulstu.core.model.ErrorConstants; + +public class ResponseExtended extends ResponseEntity { + + public ResponseExtended(ErrorConstants error, E errorData) { + super(new ControllerResponse(new ControllerResponseError(error, errorData)), HttpStatus.OK); + } +} diff --git a/src/main/java/ru/ulstu/core/repository/JpaDetachableRepository.java b/src/main/java/ru/ulstu/core/repository/JpaDetachableRepository.java new file mode 100644 index 0000000..2bf1c04 --- /dev/null +++ b/src/main/java/ru/ulstu/core/repository/JpaDetachableRepository.java @@ -0,0 +1,11 @@ +package ru.ulstu.core.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.NoRepositoryBean; + +import java.io.Serializable; + +@NoRepositoryBean +public interface JpaDetachableRepository extends JpaRepository { + void detach(T t); +} diff --git a/src/main/java/ru/ulstu/core/repository/JpaDetachableRepositoryImpl.java b/src/main/java/ru/ulstu/core/repository/JpaDetachableRepositoryImpl.java new file mode 100644 index 0000000..097a2a5 --- /dev/null +++ b/src/main/java/ru/ulstu/core/repository/JpaDetachableRepositoryImpl.java @@ -0,0 +1,22 @@ +package ru.ulstu.core.repository; + +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; + +import javax.persistence.EntityManager; +import java.io.Serializable; + +public class JpaDetachableRepositoryImpl extends SimpleJpaRepository + implements JpaDetachableRepository { + private EntityManager entityManager; + + public JpaDetachableRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) { + super(entityInformation, entityManager); + this.entityManager = entityManager; + } + + @Override + public void detach(T t) { + entityManager.detach(t); + } +} diff --git a/src/main/java/ru/ulstu/core/service/TreeService.java b/src/main/java/ru/ulstu/core/service/TreeService.java new file mode 100644 index 0000000..1e3d4a0 --- /dev/null +++ b/src/main/java/ru/ulstu/core/service/TreeService.java @@ -0,0 +1,31 @@ +package ru.ulstu.core.service; + +import org.springframework.stereotype.Service; +import ru.ulstu.core.model.TreeDto; +import ru.ulstu.core.model.TreeEntity; + +import java.util.List; +import java.util.function.Predicate; + +@Service +public class TreeService { + public TreeDto getTree(String rootName, List rootItems) { + return addChildNode(new TreeDto(rootName), rootItems, element -> true); + } + + public TreeDto getTree(String rootName, List rootItems, Predicate filterPredicate) { + return addChildNode(new TreeDto(rootName), rootItems, filterPredicate); + } + + private TreeDto addChildNode(TreeDto currentRoot, List children, Predicate filterPredicate) { + if (children != null) { + children.stream() + .filter(filterPredicate) + .forEach(item -> { + TreeDto newNode = new TreeDto(item); + currentRoot.getChildren().add(addChildNode(newNode, item.getChildren(), filterPredicate)); + }); + } + return currentRoot; + } +} diff --git a/src/main/java/ru/ulstu/core/service/XlsDocumentBuilder.java b/src/main/java/ru/ulstu/core/service/XlsDocumentBuilder.java new file mode 100644 index 0000000..d70d86e --- /dev/null +++ b/src/main/java/ru/ulstu/core/service/XlsDocumentBuilder.java @@ -0,0 +1,207 @@ +package ru.ulstu.core.service; + +import org.apache.poi.POIXMLDocument; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.RegionUtil; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import ru.ulstu.core.error.XlsParseException; + +import java.io.*; + +public class XlsDocumentBuilder { + private static final int DEFAULT_SHEET_NUM = 0; + private File documentFile; + private Workbook workBook; + private Sheet currentSheet; + + /** + * Constructor for reading and writing data from/to *.[xls|xlsx] document + * + * @param file contains existing document for reading or new document to save + */ + public XlsDocumentBuilder(File file) throws IOException, XlsParseException { + this.documentFile = file; + if (file.exists()) { + workBook = getWorkBook(file); + currentSheet = workBook.getSheetAt(DEFAULT_SHEET_NUM); + } else { + workBook = new XSSFWorkbook(); + currentSheet = workBook.createSheet(); + } + } + + private Workbook getWorkBook(File file) throws XlsParseException, IOException { + InputStream inputStream = new PushbackInputStream(new FileInputStream(file), 4096); + if (isXlsx(inputStream)) { + return new XSSFWorkbook(inputStream); + } else if (isExcel(inputStream)) { + return new HSSFWorkbook(inputStream); + } + throw new XlsParseException("Wrong document format"); + } + + /** + * Change active sheet to write or read data + * + * @param index index of sheet to activate + */ + public XlsDocumentBuilder setActiveSheet(int index) { + workBook.setActiveSheet(index); + currentSheet = workBook.getSheetAt(index); + return this; + } + + /** + * Create new sheet in document and set it active + * + * @param sheetName + */ + public XlsDocumentBuilder insertNewSheet(String sheetName) { + currentSheet = workBook.createSheet(sheetName); + workBook.setActiveSheet(getSheetCount() - 1); + return this; + } + + public XlsDocumentBuilder insertNewSheet(String sheetName, int order) { + insertNewSheet(sheetName); + workBook.setSheetOrder(sheetName, order); + return this; + } + + /** + * Returns number of sheet in document + * + * @return sheets count + */ + public int getSheetCount() { + return workBook.getNumberOfSheets(); + } + + /** + * Returns number of rows in sheet + * + * @return rows count + */ + public int getRowCount() { + return currentSheet.getLastRowNum(); + } + + /** + * Returns number of columns in sheet + * + * @return columns count + */ + public int getColumnCount() { + Row row = currentSheet.getRow(getRowCount()); + if (row == null) { + return 0; + } + return row.getLastCellNum() - 1; + } + + /** + * Returns converted to string representation of cell value + * + * @param rowIndex row index of current sheet + * @param colIndex column index of current sheet + * @return string value of cell + */ + public String getCellAsString(int rowIndex, int colIndex) { + Cell cell = currentSheet.getRow(rowIndex).getCell(colIndex); + cell.setCellType(Cell.CELL_TYPE_STRING); + return cell.getStringCellValue(); + } + + /** + * Sets string cell value + * + * @param rowIndex row index of current sheet + * @param colIndex column index of current sheet + */ + public XlsDocumentBuilder setCellValue(int rowIndex, int colIndex, String value) { + if (currentSheet.getRow(rowIndex) == null) { + currentSheet.createRow(rowIndex); + } + if (currentSheet.getRow(rowIndex).getCell(colIndex) == null) { + currentSheet.getRow(rowIndex).createCell(colIndex); + } + Cell cell = currentSheet.getRow(rowIndex).getCell(colIndex); + cell.setCellValue(value); + setColumnAutosize(colIndex, colIndex); + return this; + } + + public XlsDocumentBuilder setCellValue(int rowIndex, int colIndex, int value) { + return setCellValue(rowIndex, colIndex, String.valueOf(value)); + } + + public XlsDocumentBuilder setCellValue(int rowIndex, int colIndex, long value) { + return setCellValue(rowIndex, colIndex, String.valueOf(value)); + } + + /** + * Save current file + */ + public XlsDocumentBuilder save() throws IOException { + OutputStream out = new FileOutputStream(documentFile); + workBook.write(out); + return this; + } + + private boolean isExcel(InputStream i) throws IOException { + return (POIFSFileSystem.hasPOIFSHeader(i) || POIXMLDocument.hasOOXMLHeader(i)); + } + + private boolean isXlsx(InputStream i) throws IOException { + return POIXMLDocument.hasOOXMLHeader(i); + } + + public int getActiveSheetIndex() { + return workBook.getActiveSheetIndex(); + } + + public XlsDocumentBuilder mergeCells(int rowFrom, int rowTo, int colFrom, int colTo) { + currentSheet.addMergedRegion(new CellRangeAddress(rowFrom, rowTo, colFrom, colTo)); + return this; + } + + public void setRegionBorderWithMedium(int rowFrom, int rowTo, int colFrom, int colTo) { + for (int row = rowFrom; row < rowTo; row++) { + for (int col = colFrom; col <= colTo; col++) { + CellRangeAddress cellRangeAddress = new CellRangeAddress(row, row, col, col); + RegionUtil.setBorderBottom(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook); + RegionUtil.setBorderLeft(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook); + RegionUtil.setBorderRight(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook); + RegionUtil.setBorderTop(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook); + } + } + } + + public XlsDocumentBuilder setColumnAutosize(int from, int to) { + for (int col = from; col <= to; col++) { + currentSheet.autoSizeColumn(col, true); + } + return this; + } + + public XlsDocumentBuilder setRowAutosize(int from, int to) { + CellStyle style = workBook.createCellStyle(); + style.setWrapText(true); + for (int row = from; row <= to; row++) { + for (int col = 0; col <= currentSheet.getRow(row).getLastCellNum(); col++) { + if (currentSheet.getRow(row).getCell(col) != null) { + currentSheet.getRow(row).getCell(col).setCellStyle(style); + } + } + } + return this; + } + + public XlsDocumentBuilder deleteSheet(int index) { + workBook.removeSheetAt(index); + return this; + } +} diff --git a/src/main/java/ru/ulstu/core/util/DateUtils.java b/src/main/java/ru/ulstu/core/util/DateUtils.java new file mode 100644 index 0000000..a8853b4 --- /dev/null +++ b/src/main/java/ru/ulstu/core/util/DateUtils.java @@ -0,0 +1,46 @@ +package ru.ulstu.core.util; + +import java.time.*; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class DateUtils { + + public static Date clearTime(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + public static Calendar getCalendar(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal; + } + + public static List getMonths () { + return Arrays.asList(Month.values()); + } + + public static Date instantToDate(Instant instant) { + return Date.from(instant); + } + + public static Date localDateToDate(LocalDate localDate) { + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + public static Date localDateTimeToDate(LocalDateTime localDate) { + return Date.from(localDate.atZone(ZoneId.systemDefault()).toInstant()); + } + + public static Date localTimeToDate(LocalTime localTime) { + return Date.from(localTime.atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant()); + } +} diff --git a/src/main/java/ru/ulstu/core/util/NumberUtils.java b/src/main/java/ru/ulstu/core/util/NumberUtils.java new file mode 100644 index 0000000..5e002cd --- /dev/null +++ b/src/main/java/ru/ulstu/core/util/NumberUtils.java @@ -0,0 +1,17 @@ +package ru.ulstu.core.util; + +public class NumberUtils { + public static Double ceil(Double number) { + if (number == null) { + return 0.0; + } + return Double.valueOf(Math.ceil(number)); + } + + public static Double round(Double number) { + if (number == null) { + return 0.0; + } + return Double.valueOf(Math.ceil(number * 100)) / 100; + } +} diff --git a/src/main/java/ru/ulstu/core/util/StreamApiUtils.java b/src/main/java/ru/ulstu/core/util/StreamApiUtils.java new file mode 100644 index 0000000..282601c --- /dev/null +++ b/src/main/java/ru/ulstu/core/util/StreamApiUtils.java @@ -0,0 +1,12 @@ +package ru.ulstu.core.util; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class StreamApiUtils { + + public static List convert(List entitites, Function converter) { + return entitites.stream().map(e -> converter.apply(e)).collect(Collectors.toList()); + } +} diff --git a/src/main/java/ru/ulstu/odin/controller/OdinController.java b/src/main/java/ru/ulstu/odin/controller/OdinController.java new file mode 100644 index 0000000..8d4fcf4 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/controller/OdinController.java @@ -0,0 +1,37 @@ +package ru.ulstu.odin.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import ru.ulstu.core.model.response.Response; +import ru.ulstu.odin.model.OdinDto; +import ru.ulstu.odin.model.OdinMetadata; +import ru.ulstu.odin.service.OdinService; + +public abstract class OdinController { + public static final String META_LIST_URL = "/meta/list"; + public static final String META_ELEMENT_URL = "/meta/element"; + + private Class listDtoClass; + private Class elementDtoClass; + @Autowired + private OdinService odinService; + + public OdinController(Class listDtoClass) { + this(listDtoClass, null); + } + + public OdinController(Class listDtoClass, Class elementDtoClass) { + this.listDtoClass = listDtoClass; + this.elementDtoClass = elementDtoClass; + } + + @GetMapping(META_LIST_URL) + public Response getListModel() { + return new Response<>(odinService.getListModel(listDtoClass)); + } + + @GetMapping(META_ELEMENT_URL) + public Response getElementModel() { + return new Response<>(odinService.getElementModel(elementDtoClass)); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/odin/model/OdinBooleanField.java b/src/main/java/ru/ulstu/odin/model/OdinBooleanField.java new file mode 100644 index 0000000..02ab547 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinBooleanField.java @@ -0,0 +1,9 @@ +package ru.ulstu.odin.model; + +import java.lang.reflect.Field; + +public class OdinBooleanField extends OdinField { + public OdinBooleanField(Field field) { + super(field, OdinFieldType.BOOLEAN); + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinCollectionField.java b/src/main/java/ru/ulstu/odin/model/OdinCollectionField.java new file mode 100644 index 0000000..171e78c --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinCollectionField.java @@ -0,0 +1,28 @@ +package ru.ulstu.odin.model; + +import ru.ulstu.core.error.OdinException; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class OdinCollectionField extends OdinField { + private final String path; + + public OdinCollectionField(Field field) { + super(field, OdinFieldType.COLLECTION); + ParameterizedType genericType = (ParameterizedType) field.getGenericType(); + Type fieldElementClass = genericType.getActualTypeArguments()[0]; + try { + OdinDto someInstance = (OdinDto) ((Class) (fieldElementClass)).newInstance(); + this.path = someInstance.getControllerPath(); + } catch (IllegalAccessException | InstantiationException e) { + throw new OdinException(String.format("Can't create new instance, check default constructor of %s", + fieldElementClass.getTypeName())); + } + } + + public String getPath() { + return path; + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinDateField.java b/src/main/java/ru/ulstu/odin/model/OdinDateField.java new file mode 100644 index 0000000..9d7b037 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinDateField.java @@ -0,0 +1,18 @@ +package ru.ulstu.odin.model; + +import ru.ulstu.odin.model.annotation.OdinDate; + +import java.lang.reflect.Field; + +public class OdinDateField extends OdinField { + private final OdinDate.OdinDateType type; + + public OdinDateField(Field field) { + super(field, OdinFieldType.DATE); + this.type = getValue(OdinDate.class, "type", OdinDate.OdinDateType.class); + } + + public String getType() { + return type.toString(); + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinDto.java b/src/main/java/ru/ulstu/odin/model/OdinDto.java new file mode 100644 index 0000000..12d4286 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinDto.java @@ -0,0 +1,14 @@ +package ru.ulstu.odin.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public interface OdinDto { + Object getId(); + + @JsonProperty("view") + String getViewValue(); + + @JsonIgnore + String getControllerPath(); +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinField.java b/src/main/java/ru/ulstu/odin/model/OdinField.java new file mode 100644 index 0000000..5917a44 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinField.java @@ -0,0 +1,154 @@ +package ru.ulstu.odin.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; +import ru.ulstu.core.error.OdinException; +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.odin.model.annotation.OdinReadOnly; +import ru.ulstu.odin.model.annotation.OdinVisible; + +import javax.validation.constraints.NotNull; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.Optional; + +public abstract class OdinField implements Comparable { + public enum OdinFieldType { + BOOLEAN, + DATE, + NUMERIC, + STRING, + COLLECTION, + OBJECT, + UNKNOWN; + + @Override + public String toString() { + return this.name().toLowerCase(); + } + } + + private Field field; + protected final OdinFieldType fieldType; + protected final String fieldName; + protected final String caption; + protected final OdinVisible.OdinVisibleType visible; + protected final boolean readOnly; + protected final boolean notEmpty; + + public OdinField(Field field, OdinFieldType fieldType) { + this.field = field; + this.fieldName = getFieldName(field); + this.caption = Optional.ofNullable(getValueFromAnnotation(OdinCaption.class, "value")) + .map(value -> cast(value, String.class)) + .orElse(fieldName); + this.visible = getValue(OdinVisible.class, "type", OdinVisible.OdinVisibleType.class); + this.readOnly = field.isAnnotationPresent(OdinReadOnly.class); + this.notEmpty = field.isAnnotationPresent(NotBlank.class) + || field.isAnnotationPresent(NotEmpty.class) + || field.isAnnotationPresent(NotNull.class); + this.fieldType = fieldType; + } + + private String getFieldName(Field field) { + if (field.isAnnotationPresent(JsonProperty.class)) { + final String fieldName = cast(getValueFromAnnotation(JsonProperty.class, "value"), String.class); + return Optional.ofNullable(fieldName).filter(value -> !value.isEmpty()).orElse(field.getName()); + } + return field.getName(); + } + + private Object getDefaultValueFromAnnotation(Class annotationClass, String valueName) { + try { + final Method method = annotationClass.getDeclaredMethod(valueName); + return method.getDefaultValue(); + } catch (NoSuchMethodException e) { + throw new OdinException(String.format("Default value %s is not found in annotation %s", + valueName, annotationClass.getSimpleName())); + } + } + + private Object getValueFromAnnotation(Class annotationClass, String valueName) { + if (!field.isAnnotationPresent(annotationClass)) { + return null; + } + try { + final Method method = annotationClass.getDeclaredMethod(valueName); + return method.invoke(field.getAnnotation(annotationClass)); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new OdinException(String.format("Value %s is not found in annotation %s", + valueName, annotationClass.getSimpleName())); + } + } + + private T cast(Object value, Class clazz) { + try { + return clazz.cast(value); + } catch (ClassCastException e) { + return null; + } + } + + protected T getValue(Class annotationClass, String valueName, Class clazz) { + if (field.isAnnotationPresent(annotationClass)) { + return cast(getValueFromAnnotation(annotationClass, valueName), clazz); + } + return cast(getDefaultValueFromAnnotation(annotationClass, valueName), clazz); + } + + public String getFieldType() { + return fieldType.toString(); + } + + public String getFieldName() { + return fieldName; + } + + public String getCaption() { + return caption; + } + + public String getVisible() { + return visible.toString(); + } + + public boolean isReadOnly() { + return readOnly; + } + + public boolean isNotEmpty() { + return notEmpty; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OdinField odinField = (OdinField) o; + return Objects.equals(fieldName, odinField.fieldName); + } + + @Override + public int hashCode() { + return Objects.hash(fieldName); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " {" + + "fieldName='" + fieldName + '\'' + + ", fieldType='" + fieldType + '\'' + + '}'; + } + + @Override + public int compareTo(Object o) { + final String thisName = Optional.ofNullable(fieldName).orElse(""); + final String oName = Optional.ofNullable(o).map(obj -> ((OdinField) obj).fieldName).orElse(""); + return thisName.compareTo(oName); + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinMetadata.java b/src/main/java/ru/ulstu/odin/model/OdinMetadata.java new file mode 100644 index 0000000..fff333a --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinMetadata.java @@ -0,0 +1,26 @@ +package ru.ulstu.odin.model; + +import ru.ulstu.core.error.OdinException; + +import java.util.List; + +public class OdinMetadata { + private final boolean odinDto; + private final List fields; + + public OdinMetadata(boolean odinDto, List fields) { + if (fields == null) { + throw new OdinException("Fields list can't be null"); + } + this.odinDto = odinDto; + this.fields = fields; + } + + public boolean isOdinDto() { + return odinDto; + } + + public List getFields() { + return fields; + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinNumericField.java b/src/main/java/ru/ulstu/odin/model/OdinNumericField.java new file mode 100644 index 0000000..0739745 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinNumericField.java @@ -0,0 +1,30 @@ +package ru.ulstu.odin.model; + +import ru.ulstu.odin.model.annotation.OdinNumeric; + +import java.lang.reflect.Field; + +public class OdinNumericField extends OdinField { + private final boolean positiveOnly; + private final int precision; + private final int scale; + + public OdinNumericField(Field field) { + super(field, OdinFieldType.NUMERIC); + this.positiveOnly = getValue(OdinNumeric.class, "positiveOnly", Boolean.class); + this.precision = getValue(OdinNumeric.class, "precision", Integer.class); + this.scale = getValue(OdinNumeric.class, "scale", Integer.class); + } + + public boolean isPositiveOnly() { + return positiveOnly; + } + + public int getPrecision() { + return precision; + } + + public int getScale() { + return scale; + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinObjectField.java b/src/main/java/ru/ulstu/odin/model/OdinObjectField.java new file mode 100644 index 0000000..d19e748 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinObjectField.java @@ -0,0 +1,26 @@ +package ru.ulstu.odin.model; + +import ru.ulstu.core.error.OdinException; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; + +public class OdinObjectField extends OdinField { + private final String path; + + public OdinObjectField(Field field) { + super(field, OdinFieldType.OBJECT); + Type fieldElementClass = field.getType(); + try { + OdinDto someInstance = (OdinDto) ((Class) (fieldElementClass)).newInstance(); + this.path = someInstance.getControllerPath(); + } catch (IllegalAccessException | InstantiationException e) { + throw new OdinException(String.format("Can't create new instance, check default constructor of %s", + fieldElementClass.getTypeName())); + } + } + + public String getPath() { + return path; + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinStringField.java b/src/main/java/ru/ulstu/odin/model/OdinStringField.java new file mode 100644 index 0000000..5157d7e --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinStringField.java @@ -0,0 +1,37 @@ +package ru.ulstu.odin.model; + +import org.hibernate.validator.constraints.Email; +import ru.ulstu.odin.model.annotation.OdinString; +import ru.ulstu.odin.model.annotation.OdinString.OdinStringType; + +import javax.validation.constraints.Size; +import java.lang.reflect.Field; + +import static ru.ulstu.odin.model.annotation.OdinString.OdinStringType.EMAIL; + +public class OdinStringField extends OdinField { + private final int minLength; + private final int maxLength; + private final OdinStringType type; + + public OdinStringField(Field field) { + super(field, OdinFieldType.STRING); + this.minLength = getValue(Size.class, "min", Integer.class); + this.maxLength = getValue(Size.class, "max", Integer.class); + this.type = field.isAnnotationPresent(Email.class) + ? EMAIL + : getValue(OdinString.class, "type", OdinStringType.class); + } + + public int getMinLength() { + return minLength; + } + + public int getMaxLength() { + return maxLength; + } + + public String getType() { + return type.toString(); + } +} diff --git a/src/main/java/ru/ulstu/odin/model/OdinVoid.java b/src/main/java/ru/ulstu/odin/model/OdinVoid.java new file mode 100644 index 0000000..b643dae --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/OdinVoid.java @@ -0,0 +1,18 @@ +package ru.ulstu.odin.model; + +public class OdinVoid implements OdinDto { + @Override + public Object getId() { + return null; + } + + @Override + public String getViewValue() { + return null; + } + + @Override + public String getControllerPath() { + return null; + } +} diff --git a/src/main/java/ru/ulstu/odin/model/annotation/OdinCaption.java b/src/main/java/ru/ulstu/odin/model/annotation/OdinCaption.java new file mode 100644 index 0000000..a68d7d4 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/annotation/OdinCaption.java @@ -0,0 +1,14 @@ +package ru.ulstu.odin.model.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {FIELD, ANNOTATION_TYPE}) +public @interface OdinCaption { + String value(); +} diff --git a/src/main/java/ru/ulstu/odin/model/annotation/OdinDate.java b/src/main/java/ru/ulstu/odin/model/annotation/OdinDate.java new file mode 100644 index 0000000..9a67841 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/annotation/OdinDate.java @@ -0,0 +1,23 @@ +package ru.ulstu.odin.model.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {FIELD, ANNOTATION_TYPE}) +public @interface OdinDate { + enum OdinDateType { + DATETIME, DATE, TIME; + + @Override + public String toString() { + return this.name().toLowerCase(); + } + } + + OdinDateType type() default OdinDateType.DATE; +} diff --git a/src/main/java/ru/ulstu/odin/model/annotation/OdinNumeric.java b/src/main/java/ru/ulstu/odin/model/annotation/OdinNumeric.java new file mode 100644 index 0000000..e9eb722 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/annotation/OdinNumeric.java @@ -0,0 +1,18 @@ +package ru.ulstu.odin.model.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {FIELD, ANNOTATION_TYPE}) +public @interface OdinNumeric { + boolean positiveOnly() default false; + + int precision() default 10; + + int scale() default 0; +} diff --git a/src/main/java/ru/ulstu/odin/model/annotation/OdinReadOnly.java b/src/main/java/ru/ulstu/odin/model/annotation/OdinReadOnly.java new file mode 100644 index 0000000..e18732b --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/annotation/OdinReadOnly.java @@ -0,0 +1,13 @@ +package ru.ulstu.odin.model.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {FIELD, ANNOTATION_TYPE}) +public @interface OdinReadOnly { +} diff --git a/src/main/java/ru/ulstu/odin/model/annotation/OdinString.java b/src/main/java/ru/ulstu/odin/model/annotation/OdinString.java new file mode 100644 index 0000000..61360e1 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/annotation/OdinString.java @@ -0,0 +1,23 @@ +package ru.ulstu.odin.model.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {FIELD, ANNOTATION_TYPE}) +public @interface OdinString { + enum OdinStringType { + STRING, PASSWORD, TEXT, EMAIL, HREF; + + @Override + public String toString() { + return this.name().toLowerCase(); + } + } + + OdinStringType type() default OdinStringType.STRING; +} diff --git a/src/main/java/ru/ulstu/odin/model/annotation/OdinVisible.java b/src/main/java/ru/ulstu/odin/model/annotation/OdinVisible.java new file mode 100644 index 0000000..14f82d7 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/model/annotation/OdinVisible.java @@ -0,0 +1,23 @@ +package ru.ulstu.odin.model.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {FIELD, ANNOTATION_TYPE}) +public @interface OdinVisible { + enum OdinVisibleType { + ALL, ON_CREATE, ON_UPDATE, NONE; + + @Override + public String toString() { + return this.name().toLowerCase(); + } + } + + OdinVisibleType type() default OdinVisibleType.ALL; +} diff --git a/src/main/java/ru/ulstu/odin/service/OdinService.java b/src/main/java/ru/ulstu/odin/service/OdinService.java new file mode 100644 index 0000000..9de2426 --- /dev/null +++ b/src/main/java/ru/ulstu/odin/service/OdinService.java @@ -0,0 +1,101 @@ +package ru.ulstu.odin.service; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import ru.ulstu.core.error.OdinException; +import ru.ulstu.odin.model.*; +import ru.ulstu.odin.model.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class OdinService { + private final Logger log = LoggerFactory.getLogger(OdinService.class); + + private final Map aliases; + + public OdinService() { + aliases = new HashMap<>(); + aliases.put("boolean", OdinField.OdinFieldType.BOOLEAN); + aliases.put("date", OdinField.OdinFieldType.DATE); + aliases.put("instant", OdinField.OdinFieldType.DATE); + aliases.put("localdate", OdinField.OdinFieldType.DATE); + aliases.put("localtime", OdinField.OdinFieldType.DATE); + aliases.put("localdatetime", OdinField.OdinFieldType.DATE); + aliases.put("int", OdinField.OdinFieldType.NUMERIC); + aliases.put("integer", OdinField.OdinFieldType.NUMERIC); + aliases.put("double", OdinField.OdinFieldType.NUMERIC); + aliases.put("float", OdinField.OdinFieldType.NUMERIC); + aliases.put("long", OdinField.OdinFieldType.NUMERIC); + aliases.put("string", OdinField.OdinFieldType.STRING); + aliases.put("collection", OdinField.OdinFieldType.COLLECTION); + aliases.put("object", OdinField.OdinFieldType.OBJECT); + } + + private OdinField.OdinFieldType getFieldTypeByTypeAlias(String fieldType) { + return Optional.ofNullable(aliases.get(fieldType)).orElse(OdinField.OdinFieldType.UNKNOWN); + } + + private String getTypeAlias(String fieldType) { + return fieldType.replaceAll(".*\\.", "").toLowerCase().trim(); + } + + private OdinField.OdinFieldType getFieldType(Field field) { + final String typeAlias; + if (Collection.class.isAssignableFrom(field.getType())) { + typeAlias = "collection"; + } else if (OdinDto.class.isAssignableFrom(field.getType())) { + typeAlias = "object"; + } else { + typeAlias = getTypeAlias(field.getGenericType().getTypeName()); + } + return getFieldTypeByTypeAlias(typeAlias); + } + + private List getDtoMetaModel(Class dtoClass) { + return Arrays.stream(dtoClass.getDeclaredFields()) + .filter(field -> !field.isAnnotationPresent(JsonIgnore.class) + && !Modifier.isStatic(field.getModifiers())) + .map(field -> { + OdinField.OdinFieldType fieldType = getFieldType(field); + switch (fieldType) { + case BOOLEAN: + return new OdinBooleanField(field); + case DATE: + return new OdinDateField(field); + case NUMERIC: + return new OdinNumericField(field); + case STRING: + return new OdinStringField(field); + case COLLECTION: + return new OdinCollectionField(field); + case OBJECT: + return new OdinObjectField(field); + default: + log.debug("Unknown type {}. Skip field {} of DTO {}.", + field.getGenericType().getTypeName(), field.getName(), dtoClass.getSimpleName()); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public OdinMetadata getListModel(Class listDtoClass) { + if (listDtoClass == null) { + throw new OdinException("List DTO class is null"); + } + return new OdinMetadata(OdinDto.class.isAssignableFrom(listDtoClass), getDtoMetaModel(listDtoClass)); + } + + public OdinMetadata getElementModel(Class elementDtoClass) { + return elementDtoClass == null + ? null : + new OdinMetadata(true, getDtoMetaModel(elementDtoClass)); + } +} diff --git a/src/main/java/ru/ulstu/odinexample/controller/OdinExampleController.java b/src/main/java/ru/ulstu/odinexample/controller/OdinExampleController.java new file mode 100644 index 0000000..0101531 --- /dev/null +++ b/src/main/java/ru/ulstu/odinexample/controller/OdinExampleController.java @@ -0,0 +1,4 @@ +package ru.ulstu.odinexample.controller; + +public class OdinExampleController { +} diff --git a/src/main/java/ru/ulstu/odinexample/model/OdinExampleDto.java b/src/main/java/ru/ulstu/odinexample/model/OdinExampleDto.java new file mode 100644 index 0000000..027d96f --- /dev/null +++ b/src/main/java/ru/ulstu/odinexample/model/OdinExampleDto.java @@ -0,0 +1,4 @@ +package ru.ulstu.odinexample.model; + +public class OdinExampleDto { +} diff --git a/src/main/java/ru/ulstu/odinexample/model/OdinExampleListDto.java b/src/main/java/ru/ulstu/odinexample/model/OdinExampleListDto.java new file mode 100644 index 0000000..6924519 --- /dev/null +++ b/src/main/java/ru/ulstu/odinexample/model/OdinExampleListDto.java @@ -0,0 +1,103 @@ +package ru.ulstu.odinexample.model; + +import ru.ulstu.core.util.DateUtils; +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.odin.model.annotation.OdinDate; +import ru.ulstu.odin.model.annotation.OdinNumeric; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Date; + +public class OdinExampleListDto { + @OdinCaption("instant") + @OdinDate(type = OdinDate.OdinDateType.DATETIME) + private Instant instant; + @OdinCaption("date") + private Date date; + @OdinCaption("localdate") + private LocalDate localDate; + @OdinCaption("localtime") + @OdinDate(type = OdinDate.OdinDateType.TIME) + private LocalTime localTime; + @OdinCaption("localdatetime") + @OdinDate(type = OdinDate.OdinDateType.DATETIME) + private LocalDateTime localDateTime; + @OdinCaption("int") + private int intval; + @OdinCaption("int+settings") + @OdinNumeric(precision = 5, scale = 2) + private int intvalset; + @OdinCaption("float") + private float floatval; + @OdinCaption("double") + private double aDouble; + @OdinCaption("double+set") + @OdinNumeric(precision = 5, scale = 3) + private double aDoubles; + @OdinCaption("int+positive") + @OdinNumeric(positiveOnly = true, scale = 2) + private int invalpos; + + public OdinExampleListDto() { + this.instant = Instant.now(); + this.date = new Date(); + this.localDate = LocalDate.now(); + this.localTime = LocalTime.now(); + this.localDateTime = LocalDateTime.now(); + intval = -134; + intvalset = 1343423232; + floatval = 2323.44F; + aDouble = -232323.43434; + aDoubles = 0.456456456; + invalpos = -23232323; + } + + + public Date getInstant() { + return DateUtils.instantToDate(instant); + } + + public Date getDate() { + return date; + } + + public Date getLocalDate() { + return DateUtils.localDateToDate(localDate); + } + + public Date getLocalTime() { + return DateUtils.localTimeToDate(localTime); + } + + public Date getLocalDateTime() { + return DateUtils.localDateTimeToDate(localDateTime); + } + + public int getIntval() { + return intval; + } + + public int getIntvalset() { + return intvalset; + } + + public float getFloatval() { + return floatval; + } + + public double getaDouble() { + return aDouble; + } + + public double getaDoubles() { + return aDoubles; + } + + public int getInvalpos() { + return invalpos; + } + +} diff --git a/src/main/java/ru/ulstu/odinexample/service/OdinExampleService.java b/src/main/java/ru/ulstu/odinexample/service/OdinExampleService.java new file mode 100644 index 0000000..57cea21 --- /dev/null +++ b/src/main/java/ru/ulstu/odinexample/service/OdinExampleService.java @@ -0,0 +1,4 @@ +package ru.ulstu.odinexample.service; + +public class OdinExampleService { +} diff --git a/src/main/java/ru/ulstu/user/component/IpAddressResolver.java b/src/main/java/ru/ulstu/user/component/IpAddressResolver.java new file mode 100644 index 0000000..4c74923 --- /dev/null +++ b/src/main/java/ru/ulstu/user/component/IpAddressResolver.java @@ -0,0 +1,23 @@ +package ru.ulstu.user.component; + +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +public final class IpAddressResolver { + private static final String CLIENT_IP_HEADER = "Client-IP"; + private static final String FORWARDED_FOR_HEADER = "X-Forwarded-For"; + + public static String getRemoteAddr(HttpServletRequest request) { + String headerClientIp = request.getHeader(""); + String headerXForwardedFor = request.getHeader(HttpServletRequest.FORM_AUTH); + if (StringUtils.isEmpty(request.getRemoteAddr()) && !StringUtils.isEmpty(headerClientIp)) { + return headerClientIp; + } + if (!StringUtils.isEmpty(headerXForwardedFor)) { + return headerXForwardedFor; + } + return request.getRemoteAddr(); + } + +} diff --git a/src/main/java/ru/ulstu/user/component/UserSessionLoginHandler.java b/src/main/java/ru/ulstu/user/component/UserSessionLoginHandler.java new file mode 100644 index 0000000..75187dc --- /dev/null +++ b/src/main/java/ru/ulstu/user/component/UserSessionLoginHandler.java @@ -0,0 +1,45 @@ +package ru.ulstu.user.component; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import ru.ulstu.configuration.Constants; +import ru.ulstu.user.service.UserSessionService; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +@Component +public class UserSessionLoginHandler extends SavedRequestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + private final Logger log = LoggerFactory.getLogger(UserSessionLoginHandler.class); + private final UserSessionService userSessionService; + + public UserSessionLoginHandler(UserSessionService userSessionService) { + super(); + this.userSessionService = userSessionService; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + super.onAuthenticationSuccess(request, response, authentication); + final String login = authentication.getName(); + final String ipAddress = IpAddressResolver.getRemoteAddr(request); + final String host = request.getRemoteHost(); + log.debug("Authentication Success for {}@{} ({})", login, ipAddress, host); + HttpSession session = request.getSession(false); + if (session != null) { + final String sessionId = session.getId(); + userSessionService.createUserSession(sessionId, login, ipAddress, host); + session.setAttribute(Constants.SESSION_ID_ATTR, sessionId); + session.setMaxInactiveInterval(Constants.SESSION_TIMEOUT_SECONDS); + } + } +} diff --git a/src/main/java/ru/ulstu/user/component/UserSessionLogoutHandler.java b/src/main/java/ru/ulstu/user/component/UserSessionLogoutHandler.java new file mode 100644 index 0000000..606cbcd --- /dev/null +++ b/src/main/java/ru/ulstu/user/component/UserSessionLogoutHandler.java @@ -0,0 +1,49 @@ +package ru.ulstu.user.component; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.stereotype.Component; +import ru.ulstu.configuration.Constants; +import ru.ulstu.user.service.UserSessionService; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +@Component +public class UserSessionLogoutHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler { + private final Logger log = LoggerFactory.getLogger(UserSessionLogoutHandler.class); + private final UserSessionService userSessionService; + + public UserSessionLogoutHandler(UserSessionService userSessionService) { + this.userSessionService = userSessionService; + setDefaultTargetUrl(Constants.LOGOUT_URL); + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + if (authentication == null) { + super.onLogoutSuccess(request, response, authentication); + return; + } + final String login = authentication.getName(); + final String ipAddress = IpAddressResolver.getRemoteAddr(request); + final String host = request.getRemoteHost(); + log.debug("Logout Success for {}@{} ({})", login, ipAddress, host); + HttpSession session = request.getSession(false); + if (session != null) { + final String sessionId = session.getAttribute(Constants.SESSION_ID_ATTR).toString(); + userSessionService.closeUserSession(sessionId); + session.removeAttribute(Constants.SESSION_ID_ATTR); + session.invalidate(); + } + super.onLogoutSuccess(request, response, authentication); + } +} diff --git a/src/main/java/ru/ulstu/user/controller/UserController.java b/src/main/java/ru/ulstu/user/controller/UserController.java new file mode 100644 index 0000000..f9f49e3 --- /dev/null +++ b/src/main/java/ru/ulstu/user/controller/UserController.java @@ -0,0 +1,158 @@ +package ru.ulstu.user.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; +import ru.ulstu.configuration.Constants; +import ru.ulstu.core.model.response.PageableItems; +import ru.ulstu.core.model.response.Response; +import ru.ulstu.odin.controller.OdinController; +import ru.ulstu.odin.model.OdinMetadata; +import ru.ulstu.odin.model.OdinVoid; +import ru.ulstu.odin.service.OdinService; +import ru.ulstu.user.model.*; +import ru.ulstu.user.service.UserService; +import ru.ulstu.user.service.UserSessionService; +import ru.ulstu.user.model.*; + +import javax.validation.Valid; + +import static ru.ulstu.user.controller.UserController.URL; + +@RestController +@RequestMapping(URL) +public class UserController extends OdinController { + public static final String URL = Constants.API_1_0 + "users"; + public static final String ROLES_URL = "/roles"; + public static final String ROLES_META_URL = ROLES_URL + OdinController.META_LIST_URL; + public static final String SESSIONS_URL = "/sessions"; + public static final String SESSIONS_META_URL = SESSIONS_URL + OdinController.META_LIST_URL; + public static final String REGISTER_URL = "/register"; + public static final String ACTIVATE_URL = "/activate"; + public static final String PASSWORD_RESET_REQUEST_URL = "/password-reset-request"; + public static final String PASSWORD_RESET_URL = "/password-reset"; + + private final Logger log = LoggerFactory.getLogger(UserController.class); + + private final UserService userService; + private final UserSessionService userSessionService; + private final OdinService odinRolesService; + private final OdinService odinSessionsService; + + public UserController(UserService userService, + UserSessionService userSessionService, + OdinService odinRolesService, + OdinService odinSessionsService) { + super(UserListDto.class, UserDto.class); + this.userService = userService; + this.userSessionService = userSessionService; + this.odinRolesService = odinRolesService; + this.odinSessionsService = odinSessionsService; + } + + @GetMapping(ROLES_URL) + @Secured(UserRoleConstants.ADMIN) + public Response> getUserRoles() { + log.debug("REST: UserController.getUserRoles()"); + return new Response<>(userService.getUserRoles()); + } + + @GetMapping(ROLES_META_URL) + @Secured(UserRoleConstants.ADMIN) + public Response getUserRolesMetaData() { + log.debug("REST: UserController.getUserRolesMetaData()"); + return new Response<>(odinRolesService.getListModel(UserRoleDto.class)); + } + + @GetMapping(SESSIONS_URL) + @Secured(UserRoleConstants.ADMIN) + public Response> getUserSessions(@RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "count", defaultValue = "10") int count) { + log.debug("REST: UserController.getUserSessions()"); + return new Response<>(userSessionService.getSessions(offset, count)); + } + + @GetMapping(SESSIONS_META_URL) + @Secured(UserRoleConstants.ADMIN) + public Response getUserSessionsMetaData() { + log.debug("REST: UserController.getUserSessionsMetaData()"); + return new Response<>(odinSessionsService.getListModel(UserSessionListDto.class)); + } + + @GetMapping("") + @Secured(UserRoleConstants.ADMIN) + public Response> getAllUsers(@RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "count", defaultValue = "10") int count) { + log.debug("REST: UserController.getAllUsers( {}, {} )", offset, count); + return new Response<>(userService.getAllUsers(offset, count)); + } + + @GetMapping("/{userId}") + @Secured(UserRoleConstants.ADMIN) + public Response getUser(@PathVariable Integer userId) { + log.debug("REST: UserController.getUser( {} )", userId); + return new Response<>(userService.getUserWithRolesById(userId)); + } + + + @PostMapping("") + @Secured(UserRoleConstants.ADMIN) + public Response createUser(@Valid @RequestBody UserDto userDto) { + log.debug("REST: UserController.createUser( {} )", userDto.getLogin()); + return new Response<>(userService.createUser(userDto)); + } + + @PutMapping("") + @Secured(UserRoleConstants.ADMIN) + public Response updateUser(@Valid @RequestBody UserDto userDto) { + log.debug("REST: UserController.updateUser( {} )", userDto.getLogin()); + return new Response<>(userService.updateUser(userDto)); + } + + @DeleteMapping("/{userId}") + @Secured(UserRoleConstants.ADMIN) + public Response deleteUser(@PathVariable Integer userId) { + log.debug("REST: UserController.deleteUser( {} )", userId); + return new Response<>(userService.deleteUser(userId)); + } + + @PostMapping(REGISTER_URL) + public Response registerUser(@Valid @RequestBody UserDto userDto) { + log.debug("REST: UserController.registerUser( {} )", userDto.getLogin()); + return new Response<>(userService.createUser(userDto)); + } + + @PostMapping(ACTIVATE_URL) + public Response activateUser(@RequestParam("key") String activationKey) { + log.debug("REST: UserController.activateUser( {} )", activationKey); + return new Response<>(userService.activateUser(activationKey)); + } + + // TODO: add page for user edit (user-profile) + @PostMapping("/change-information") + public Response changeInformation(@Valid @RequestBody UserDto userDto) { + log.debug("REST: UserController.changeInformation( {} )", userDto.getLogin()); + return new Response<>(userService.updateUserInformation(userDto)); + } + + // TODO: add page for user password change (user-profile) + @PostMapping("/change-password") + public Response changePassword(@Valid @RequestBody UserDto userDto) { + log.debug("REST: UserController.changePassword( {} )", userDto.getLogin()); + return new Response<>(userService.changeUserPassword(userDto)); + } + + @PostMapping(PASSWORD_RESET_REQUEST_URL) + public Response requestPasswordReset(@RequestParam("email") String email) { + log.debug("REST: UserController.requestPasswordReset( {} )", email); + return new Response<>(userService.requestUserPasswordReset(email)); + } + + @PostMapping(PASSWORD_RESET_URL) + public Response finishPasswordReset(@RequestParam("key") String key, + @RequestBody UserResetPasswordDto userResetPasswordDto) { + log.debug("REST: UserController.requestPasswordReset( {} )", key); + return new Response<>(userService.completeUserPasswordReset(key, userResetPasswordDto)); + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserActivationError.java b/src/main/java/ru/ulstu/user/error/UserActivationError.java new file mode 100644 index 0000000..31cd2c4 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserActivationError.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.error; + +public class UserActivationError extends RuntimeException { + public UserActivationError(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserEmailExistsException.java b/src/main/java/ru/ulstu/user/error/UserEmailExistsException.java new file mode 100644 index 0000000..2864dc5 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserEmailExistsException.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.error; + +public class UserEmailExistsException extends RuntimeException { + public UserEmailExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserIdExistsException.java b/src/main/java/ru/ulstu/user/error/UserIdExistsException.java new file mode 100644 index 0000000..f6a15b5 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserIdExistsException.java @@ -0,0 +1,6 @@ +package ru.ulstu.user.error; + +public class UserIdExistsException extends RuntimeException { + public UserIdExistsException() { + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserIsUndeadException.java b/src/main/java/ru/ulstu/user/error/UserIsUndeadException.java new file mode 100644 index 0000000..1c727e0 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserIsUndeadException.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.error; + +public class UserIsUndeadException extends RuntimeException { + public UserIsUndeadException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserLoginExistsException.java b/src/main/java/ru/ulstu/user/error/UserLoginExistsException.java new file mode 100644 index 0000000..c6edbbf --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserLoginExistsException.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.error; + +public class UserLoginExistsException extends RuntimeException { + public UserLoginExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserNotActivatedException.java b/src/main/java/ru/ulstu/user/error/UserNotActivatedException.java new file mode 100644 index 0000000..4931898 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserNotActivatedException.java @@ -0,0 +1,6 @@ +package ru.ulstu.user.error; + +public class UserNotActivatedException extends RuntimeException { + public UserNotActivatedException() { + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserNotFoundException.java b/src/main/java/ru/ulstu/user/error/UserNotFoundException.java new file mode 100644 index 0000000..a3b6fdf --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.error; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserPasswordsNotValidOrNotMatchException.java b/src/main/java/ru/ulstu/user/error/UserPasswordsNotValidOrNotMatchException.java new file mode 100644 index 0000000..088f999 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserPasswordsNotValidOrNotMatchException.java @@ -0,0 +1,6 @@ +package ru.ulstu.user.error; + +public class UserPasswordsNotValidOrNotMatchException extends RuntimeException { + public UserPasswordsNotValidOrNotMatchException() { + } +} diff --git a/src/main/java/ru/ulstu/user/error/UserResetKeyError.java b/src/main/java/ru/ulstu/user/error/UserResetKeyError.java new file mode 100644 index 0000000..73ab6e1 --- /dev/null +++ b/src/main/java/ru/ulstu/user/error/UserResetKeyError.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.error; + +public class UserResetKeyError extends RuntimeException { + public UserResetKeyError(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/model/User.java b/src/main/java/ru/ulstu/user/model/User.java new file mode 100644 index 0000000..8d5bce8 --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/User.java @@ -0,0 +1,170 @@ +package ru.ulstu.user.model; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.validator.constraints.Email; +import ru.ulstu.configuration.Constants; +import ru.ulstu.core.model.BaseEntity; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "users") +public class User extends BaseEntity { + @NotNull + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + @Column(length = 50, unique = true, nullable = false) + private String login; + + @NotNull + @Size(min = 60, max = 60) + @Column(name = "password_hash", length = 60, nullable = false) + private String password; + + @NotNull + @Size(max = 50) + @Column(name = "first_name", length = 50, nullable = false) + private String firstName; + + @NotNull + @Size(max = 50) + @Column(name = "last_name", length = 50, nullable = false) + private String lastName; + + @NotNull + @Email + @Size(min = 5, max = 100) + @Column(length = 100, nullable = false, unique = true) + private String email; + + @NotNull + @Column(nullable = false) + private boolean activated; + + @Size(max = 20) + @Column(name = "activation_key", length = 20) + private String activationKey; + + @Column(name = "activation_date") + @Temporal(TemporalType.TIMESTAMP) + private Date activationDate; + + @Size(max = 20) + @Column(name = "reset_key", length = 20) + private String resetKey; + + @Column(name = "reset_date") + @Temporal(TemporalType.TIMESTAMP) + private Date resetDate; + + @ManyToMany + @JoinTable( + name = "user_role", + joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "user_role_name", referencedColumnName = "name")}) + @BatchSize(size = 20) + private Set roles; + + public User() { + roles = new HashSet<>(); + activated = false; + activationDate = new Date(); + resetDate = null; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login.toLowerCase(); + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean getActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getActivationKey() { + return activationKey; + } + + public void setActivationKey(String activationKey) { + this.activationKey = activationKey; + } + + public Date getActivationDate() { + return activationDate; + } + + public void setActivationDate(Date activationDate) { + this.activationDate = activationDate; + } + + public String getResetKey() { + return resetKey; + } + + public void setResetKey(String resetKey) { + this.resetKey = resetKey; + } + + public Date getResetDate() { + return resetDate; + } + + public void setResetDate(Date resetDate) { + this.resetDate = resetDate; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Collection roles) { + this.roles.clear(); + this.roles.addAll(roles); + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserDto.java b/src/main/java/ru/ulstu/user/model/UserDto.java new file mode 100644 index 0000000..c98e0bb --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserDto.java @@ -0,0 +1,193 @@ +package ru.ulstu.user.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.util.StringUtils; +import ru.ulstu.configuration.Constants; +import ru.ulstu.odin.model.OdinDto; +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.odin.model.annotation.OdinReadOnly; +import ru.ulstu.odin.model.annotation.OdinString; +import ru.ulstu.odin.model.annotation.OdinVisible; +import ru.ulstu.user.controller.UserController; + +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static ru.ulstu.odin.model.annotation.OdinString.OdinStringType.PASSWORD; + +public class UserDto implements OdinDto { + @OdinReadOnly + private Integer id; + + @NotBlank + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 4, max = 50) + @OdinCaption("Логин") + private String login; + + @NotBlank + @Size(min = 2, max = 50) + @OdinCaption("Имя") + private String firstName; + + @NotBlank + @Size(min = 2, max = 50) + @OdinCaption("Фамилия") + private String lastName; + + @Email + @NotBlank + @Size(min = 5, max = 100) + @OdinCaption("E-Mail") + private String email; + + @OdinCaption("Аккаунт активен") + private boolean activated; + + @OdinCaption("Роли") + private LinkedHashSet roles; + + @OdinString(type = PASSWORD) + @OdinVisible(type = OdinVisible.OdinVisibleType.ON_UPDATE) + @OdinCaption("Текущий пароль") + @Size(max = 50) + private String oldPassword; + + @OdinString(type = PASSWORD) + @OdinCaption("Пароль") + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String password; + + @OdinString(type = PASSWORD) + @OdinCaption("Пароль (подтверждение)") + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String passwordConfirm; + + public UserDto() { + activated = false; + roles = new LinkedHashSet<>(); + } + + public UserDto(User user) { + this(); + this.id = user.getId(); + this.login = user.getLogin(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.email = user.getEmail(); + this.activated = user.getActivated(); + this.roles.addAll(user.getRoles().stream() + .map(UserRoleDto::new) + .collect(Collectors.toList())); + } + + public Integer getId() { + return id; + } + + @Override + public String getViewValue() { + return login; + } + + @Override + public String getControllerPath() { + return UserController.URL; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Collection roles) { + this.roles.clear(); + this.roles.addAll(roles); + } + + public String getOldPassword() { + return oldPassword; + } + + public String getPassword() { + return password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + @JsonIgnore + public boolean isPasswordsValid() { + if (StringUtils.isEmpty(password) || StringUtils.isEmpty(passwordConfirm)) { + return false; + } + return Objects.equals(password, passwordConfirm); + } + + @JsonIgnore + public boolean isOldPasswordValid() { + return !StringUtils.isEmpty(oldPassword); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " {" + + "id=" + id + + ", login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", activated=" + activated + + ", roles=" + roles + + ", password='" + password + '\'' + + ", passwordConfirm='" + passwordConfirm + '\'' + + '}'; + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserListDto.java b/src/main/java/ru/ulstu/user/model/UserListDto.java new file mode 100644 index 0000000..2df2174 --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserListDto.java @@ -0,0 +1,76 @@ +package ru.ulstu.user.model; + +import ru.ulstu.odin.model.OdinDto; +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.odin.model.annotation.OdinVisible; +import ru.ulstu.user.controller.UserController; + +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; + +public class UserListDto implements OdinDto { + @OdinVisible(type = OdinVisible.OdinVisibleType.NONE) + private int id; + @OdinCaption("Логин") + private String login; + @OdinCaption("Имя") + private String firstName; + @OdinCaption("Фамилия") + private String lastName; + @OdinCaption("E-Mail") + private String email; + @OdinCaption("Аккаунт активен") + private boolean activated; + @OdinCaption("Права администратора") + private boolean admin; + + public UserListDto(User user) { + this.id = user.getId(); + this.login = user.getLogin(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.email = user.getEmail(); + this.activated = user.getActivated(); + this.admin = Optional.ofNullable(user.getRoles()).orElse(Collections.emptySet()).stream() + .anyMatch(role -> Objects.equals(UserRoleConstants.ADMIN, role.getName())); + } + + public Integer getId() { + return id; + } + + @Override + public String getViewValue() { + return login; + } + + @Override + public String getControllerPath() { + return UserController.URL; + } + + public String getLogin() { + return login; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getEmail() { + return email; + } + + public boolean isActivated() { + return activated; + } + + public boolean isAdmin() { + return admin; + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserResetPasswordDto.java b/src/main/java/ru/ulstu/user/model/UserResetPasswordDto.java new file mode 100644 index 0000000..33d84bc --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserResetPasswordDto.java @@ -0,0 +1,28 @@ +package ru.ulstu.user.model; + +import org.hibernate.validator.constraints.NotEmpty; +import ru.ulstu.configuration.Constants; + +import javax.validation.constraints.Size; +import java.util.Objects; + +public class UserResetPasswordDto { + @NotEmpty + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String password; + @NotEmpty + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String passwordConfirm; + + public String getPassword() { + return password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + public boolean isPasswordsValid() { + return Objects.equals(password, passwordConfirm); + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserRole.java b/src/main/java/ru/ulstu/user/model/UserRole.java new file mode 100644 index 0000000..dac996a --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserRole.java @@ -0,0 +1,46 @@ +package ru.ulstu.user.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Entity +@Table(name = "user_roles") +public class UserRole { + @Id + @NotNull + @Size(max = 50) + @Column(length = 50, nullable = false) + private String name; + + public UserRole() { + } + + public UserRole(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserRole role = (UserRole) o; + return !(name != null ? !name.equals(role.name) : role.name != null); + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserRoleConstants.java b/src/main/java/ru/ulstu/user/model/UserRoleConstants.java new file mode 100644 index 0000000..364f0d2 --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserRoleConstants.java @@ -0,0 +1,6 @@ +package ru.ulstu.user.model; + +public class UserRoleConstants { + public static final String ADMIN = "ROLE_ADMIN"; + public static final String USER = "ROLE_USER"; +} diff --git a/src/main/java/ru/ulstu/user/model/UserRoleDto.java b/src/main/java/ru/ulstu/user/model/UserRoleDto.java new file mode 100644 index 0000000..970db20 --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserRoleDto.java @@ -0,0 +1,36 @@ +package ru.ulstu.user.model; + +import ru.ulstu.odin.model.OdinDto; +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.user.controller.UserController; + +public class UserRoleDto implements OdinDto { + @OdinCaption("Роль") + private String id; + + public UserRoleDto() { + } + + public UserRoleDto(UserRole role) { + this.id = role.getName(); + } + + public UserRoleDto(String name) { + this.id = name; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getViewValue() { + return id; + } + + @Override + public String getControllerPath() { + return UserController.URL + UserController.ROLES_URL; + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserSession.java b/src/main/java/ru/ulstu/user/model/UserSession.java new file mode 100644 index 0000000..1eb761d --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserSession.java @@ -0,0 +1,75 @@ +package ru.ulstu.user.model; + +import ru.ulstu.core.model.BaseEntity; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.util.Date; + +@Entity +@Table(name = "user_sessions") +public class UserSession extends BaseEntity { + @NotNull + @Column(name = "session_id", nullable = false, unique = true) + private String sessionId; + + @NotNull + @Column(name = "ip_address", nullable = false) + private String ipAddress; + + @NotNull + @Column(nullable = false) + private String host; + + @NotNull + @Column(name = "login_time", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date loginTime; + + @Column(name = "logout_time") + @Temporal(TemporalType.TIMESTAMP) + private Date logoutTime; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id") + private User user; + + public UserSession() { + } + + public UserSession(String sessionId, String ipAddress, String host, User user) { + this.sessionId = sessionId; + this.ipAddress = ipAddress; + this.host = host; + this.loginTime = new Date(); + this.user = user; + } + + public String getSessionId() { + return sessionId; + } + + public String getIpAddress() { + return ipAddress; + } + + public String getHost() { + return host; + } + + public Date getLoginTime() { + return loginTime; + } + + public Date getLogoutTime() { + return logoutTime; + } + + public User getUser() { + return user; + } + + public void close() { + this.logoutTime = new Date(); + } +} diff --git a/src/main/java/ru/ulstu/user/model/UserSessionListDto.java b/src/main/java/ru/ulstu/user/model/UserSessionListDto.java new file mode 100644 index 0000000..ebfd653 --- /dev/null +++ b/src/main/java/ru/ulstu/user/model/UserSessionListDto.java @@ -0,0 +1,56 @@ +package ru.ulstu.user.model; + +import ru.ulstu.odin.model.annotation.OdinCaption; +import ru.ulstu.odin.model.annotation.OdinDate; + +import java.util.Date; + +public class UserSessionListDto { + @OdinCaption("Сессия") + private String sessionId; + @OdinCaption("Пользователь") + private String login; + @OdinCaption("IP адрес") + private String ipAddress; + @OdinCaption("Хост") + private String host; + @OdinCaption("Вход") + @OdinDate(type = OdinDate.OdinDateType.DATETIME) + private Date loginTime; + @OdinCaption("Выход") + @OdinDate(type = OdinDate.OdinDateType.DATETIME) + private Date logoutTime; + + public UserSessionListDto(UserSession userSession) { + this.sessionId = userSession.getSessionId(); + this.login = userSession.getUser().getLogin(); + this.ipAddress = userSession.getIpAddress(); + this.host = userSession.getHost(); + this.loginTime = userSession.getLoginTime(); + this.logoutTime = userSession.getLogoutTime(); + } + + public String getSessionId() { + return sessionId; + } + + public String getLogin() { + return login; + } + + public String getIpAddress() { + return ipAddress; + } + + public String getHost() { + return host; + } + + public Date getLoginTime() { + return loginTime; + } + + public Date getLogoutTime() { + return logoutTime; + } +} diff --git a/src/main/java/ru/ulstu/user/repository/UserRepository.java b/src/main/java/ru/ulstu/user/repository/UserRepository.java new file mode 100644 index 0000000..2edc8aa --- /dev/null +++ b/src/main/java/ru/ulstu/user/repository/UserRepository.java @@ -0,0 +1,28 @@ +package ru.ulstu.user.repository; + +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.ulstu.user.model.User; + +import java.util.Date; +import java.util.List; + +public interface UserRepository extends JpaRepository { + User findOneByActivationKey(String activationKey); + + List findAllByActivatedIsFalseAndActivationDateBefore(Date date); + + User findOneByResetKey(String resetKey); + + List findAllByResetKeyNotNullAndResetDateBefore(Date date); + + User findOneByEmailIgnoreCase(String email); + + User findOneByLoginIgnoreCase(String login); + + @EntityGraph(attributePaths = "roles") + User findOneWithRolesById(int id); + + @EntityGraph(attributePaths = "roles") + User findOneWithRolesByLogin(String login); +} diff --git a/src/main/java/ru/ulstu/user/repository/UserRoleRepository.java b/src/main/java/ru/ulstu/user/repository/UserRoleRepository.java new file mode 100644 index 0000000..098bda6 --- /dev/null +++ b/src/main/java/ru/ulstu/user/repository/UserRoleRepository.java @@ -0,0 +1,7 @@ +package ru.ulstu.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.ulstu.user.model.UserRole; + +public interface UserRoleRepository extends JpaRepository { +} diff --git a/src/main/java/ru/ulstu/user/repository/UserSessionRepository.java b/src/main/java/ru/ulstu/user/repository/UserSessionRepository.java new file mode 100644 index 0000000..b922e4f --- /dev/null +++ b/src/main/java/ru/ulstu/user/repository/UserSessionRepository.java @@ -0,0 +1,13 @@ +package ru.ulstu.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.ulstu.user.model.UserSession; + +import java.util.Date; +import java.util.List; + +public interface UserSessionRepository extends JpaRepository { + UserSession findOneBySessionId(String sessionId); + + List findAllByLogoutTimeIsNullAndLoginTimeBefore(Date date); +} diff --git a/src/main/java/ru/ulstu/user/scheduler/UserScheduler.java b/src/main/java/ru/ulstu/user/scheduler/UserScheduler.java new file mode 100644 index 0000000..bcad354 --- /dev/null +++ b/src/main/java/ru/ulstu/user/scheduler/UserScheduler.java @@ -0,0 +1,50 @@ +package ru.ulstu.user.scheduler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import ru.ulstu.core.util.DateUtils; +import ru.ulstu.user.model.User; +import ru.ulstu.user.repository.UserRepository; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Service +public class UserScheduler { + private final Logger log = LoggerFactory.getLogger(UserScheduler.class); + + private final UserRepository userRepository; + + public UserScheduler(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Scheduled(cron = "0 0 1 * * ?") + public void removeNotActivatedUsers() { + log.debug("UserScheduler.removeNotActivatedUsers started"); + List users = userRepository.findAllByActivatedIsFalseAndActivationDateBefore( + DateUtils.instantToDate(Instant.now().minus(3, ChronoUnit.DAYS))); + users.forEach(user -> { + log.debug("Deleting not activated user {}", user.getLogin()); + userRepository.delete(user); + }); + log.debug("UserScheduler.removeNotActivatedUsers finished"); + } + + @Scheduled(cron = "0 0 1 * * ?") + public void removeUnusedRestKeyRequestsOfUsers() { + log.debug("UserScheduler.removeUnusedRestKeyRequestsOfUsers started"); + List users = userRepository.findAllByResetKeyNotNullAndResetDateBefore( + DateUtils.instantToDate(Instant.now().minus(7, ChronoUnit.DAYS))); + users.forEach(user -> { + log.debug("Deleting old reset key request of user {}", user.getLogin()); + user.setResetKey(null); + user.setResetDate(null); + userRepository.save(user); + }); + log.debug("UserScheduler.removeUnusedRestKeyRequestsOfUsers finished"); + } +} diff --git a/src/main/java/ru/ulstu/user/scheduler/UserSessionScheduler.java b/src/main/java/ru/ulstu/user/scheduler/UserSessionScheduler.java new file mode 100644 index 0000000..3134fe5 --- /dev/null +++ b/src/main/java/ru/ulstu/user/scheduler/UserSessionScheduler.java @@ -0,0 +1,37 @@ +package ru.ulstu.user.scheduler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import ru.ulstu.core.util.DateUtils; +import ru.ulstu.user.model.UserSession; +import ru.ulstu.user.repository.UserSessionRepository; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Service +public class UserSessionScheduler { + private final Logger log = LoggerFactory.getLogger(UserSessionScheduler.class); + + private final UserSessionRepository userSessionRepository; + + public UserSessionScheduler(UserSessionRepository userSessionRepository) { + this.userSessionRepository = userSessionRepository; + } + + @Scheduled(cron = "0 0 1 * * ?") + public void closeOldSessions() { + log.debug("UserSessionScheduler.closeOldSessions started"); + final List sessions = userSessionRepository.findAllByLogoutTimeIsNullAndLoginTimeBefore( + DateUtils.instantToDate(Instant.now().minus(1, ChronoUnit.DAYS))); + sessions.forEach(session -> { + log.debug("Close session {}", session.getSessionId()); + session.close(); + userSessionRepository.save(session); + }); + log.debug("UserSessionScheduler.closeOldSessions finished"); + } +} diff --git a/src/main/java/ru/ulstu/user/service/MailService.java b/src/main/java/ru/ulstu/user/service/MailService.java new file mode 100644 index 0000000..0708450 --- /dev/null +++ b/src/main/java/ru/ulstu/user/service/MailService.java @@ -0,0 +1,78 @@ +package ru.ulstu.user.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.mail.MailProperties; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring4.SpringTemplateEngine; +import ru.ulstu.configuration.ApplicationProperties; +import ru.ulstu.configuration.Constants; +import ru.ulstu.user.model.User; + +import javax.mail.internet.MimeMessage; +import java.nio.charset.StandardCharsets; + +@Service +public class MailService { + private final Logger log = LoggerFactory.getLogger(MailService.class); + + private static final String USER = "user"; + private static final String BASE_URL = "baseUrl"; + + private final JavaMailSender javaMailSender; + private final SpringTemplateEngine templateEngine; + private final MailProperties mailProperties; + private final ApplicationProperties applicationProperties; + + public MailService(JavaMailSender javaMailSender, SpringTemplateEngine templateEngine, + MailProperties mailProperties, ApplicationProperties applicationProperties) { + this.javaMailSender = javaMailSender; + this.templateEngine = templateEngine; + this.mailProperties = mailProperties; + this.applicationProperties = applicationProperties; + } + + @Async + public void sendEmail(String to, String subject, String content) { + log.debug("Send email to '{}' with subject '{}'", to, subject); + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, StandardCharsets.UTF_8.name()); + message.setTo(to); + message.setFrom(mailProperties.getUsername()); + message.setSubject(subject); + message.setText(content, true); + javaMailSender.send(mimeMessage); + log.debug("Sent email to User '{}'", to); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.warn("Email could not be sent to user '{}'", to, e); + } else { + log.warn("Email could not be sent to user '{}': {}", to, e.getMessage()); + } + } + } + + @Async + public void sendEmailFromTemplate(User user, String templateName, String subject) { + Context context = new Context(); + context.setVariable(USER, user); + context.setVariable(BASE_URL, applicationProperties.getBaseUrl()); + String content = templateEngine.process(templateName, context); + sendEmail(user.getEmail(), subject, content); + } + + @Async + public void sendActivationEmail(User user) { + sendEmailFromTemplate(user, "activationEmail", Constants.MAIL_ACTIVATE); + } + + @Async + public void sendPasswordResetMail(User user) { + sendEmailFromTemplate(user, "passwordResetEmail", Constants.MAIL_RESET); + } +} diff --git a/src/main/java/ru/ulstu/user/service/UserMapper.java b/src/main/java/ru/ulstu/user/service/UserMapper.java new file mode 100644 index 0000000..5607a26 --- /dev/null +++ b/src/main/java/ru/ulstu/user/service/UserMapper.java @@ -0,0 +1,66 @@ +package ru.ulstu.user.service; + +import org.springframework.stereotype.Service; +import ru.ulstu.user.model.*; +import ru.ulstu.user.model.*; +import ru.ulstu.user.repository.UserRoleRepository; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class UserMapper { + private final UserRoleRepository userRoleRepository; + + public UserMapper(UserRoleRepository userRoleRepository) { + this.userRoleRepository = userRoleRepository; + } + + public Set rolesFromDto(Set strings) { + return Optional.ofNullable(strings).orElse(Collections.emptySet()).stream() + .filter(Objects::nonNull) + .map(role -> userRoleRepository.findOne(role.getId().toString())) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + public UserDto userEntityToUserDto(User userEntity) { + if (userEntity == null) { + return null; + } + return new UserDto(userEntity); + } + + public UserListDto userEntityToUserListDto(User userEntity) { + if (userEntity == null) { + return null; + } + return new UserListDto(userEntity); + } + + public List userEntitiesToUserListDtos(List userEntities) { + return userEntities.stream() + .filter(Objects::nonNull) + .map(this::userEntityToUserListDto) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public User userDtoToUserEntity(UserDto userDto) { + if (userDto == null) { + return null; + } + final User user = new User(); + user.setId(userDto.getId()); + user.setLogin(userDto.getLogin()); + user.setFirstName(userDto.getFirstName()); + user.setLastName(userDto.getLastName()); + user.setEmail(userDto.getEmail()); + user.setActivated(userDto.isActivated()); + final Set roles = this.rolesFromDto(userDto.getRoles()); + if (!roles.isEmpty()) { + user.setRoles(roles); + } + return user; + } +} diff --git a/src/main/java/ru/ulstu/user/service/UserService.java b/src/main/java/ru/ulstu/user/service/UserService.java new file mode 100644 index 0000000..0c51e5a --- /dev/null +++ b/src/main/java/ru/ulstu/user/service/UserService.java @@ -0,0 +1,285 @@ +package ru.ulstu.user.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import ru.ulstu.configuration.ApplicationProperties; +import ru.ulstu.core.error.EntityIdIsNullException; +import ru.ulstu.core.jpa.OffsetablePageRequest; +import ru.ulstu.core.model.BaseEntity; +import ru.ulstu.core.model.response.PageableItems; +import ru.ulstu.user.error.*; +import ru.ulstu.user.model.*; +import ru.ulstu.user.repository.UserRepository; +import ru.ulstu.user.repository.UserRoleRepository; +import ru.ulstu.user.util.UserUtils; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional +public class UserService implements UserDetailsService { + private final Logger log = LoggerFactory.getLogger(UserService.class); + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserRoleRepository userRoleRepository; + private final UserMapper userMapper; + private final MailService mailService; + private final ApplicationProperties applicationProperties; + + public UserService(UserRepository userRepository, + PasswordEncoder passwordEncoder, + UserRoleRepository userRoleRepository, + UserMapper userMapper, + MailService mailService, + ApplicationProperties applicationProperties) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userRoleRepository = userRoleRepository; + this.userMapper = userMapper; + this.mailService = mailService; + this.applicationProperties = applicationProperties; + } + + private User getUserByEmail(String email) { + return userRepository.findOneByEmailIgnoreCase(email); + } + + private User getUserByActivationKey(String activationKey) { + return userRepository.findOneByActivationKey(activationKey); + } + + public User getUserByLogin(String login) { + return userRepository.findOneByLoginIgnoreCase(login); + } + + @Transactional(readOnly = true) + public UserDto getUserWithRolesById(Integer userId) { + final User userEntity = userRepository.findOneWithRolesById(userId); + if (userEntity == null) { + throw new UserNotFoundException(userId.toString()); + } + return userMapper.userEntityToUserDto(userEntity); + } + + @Transactional(readOnly = true) + public PageableItems getAllUsers(int offset, int count) { + final Page page = userRepository.findAll(new OffsetablePageRequest(offset, count, new Sort("id"))); + return new PageableItems<>(page.getTotalElements(), userMapper.userEntitiesToUserListDtos(page.getContent())); + } + + @Transactional(readOnly = true) + public PageableItems getUserRoles() { + final List roles = userRoleRepository.findAll().stream() + .map(UserRoleDto::new) + .sorted(Comparator.comparing(UserRoleDto::getViewValue)) + .collect(Collectors.toList()); + return new PageableItems<>(roles.size(), roles); + } + + public UserDto createUser(UserDto userDto) { + if (userDto.getId() != null) { + throw new UserIdExistsException(); + } + if (getUserByLogin(userDto.getLogin()) != null) { + throw new UserLoginExistsException(userDto.getLogin()); + } + if (getUserByEmail(userDto.getEmail()) != null) { + throw new UserEmailExistsException(userDto.getEmail()); + } + if (!userDto.isPasswordsValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + User user = userMapper.userDtoToUserEntity(userDto); + user.setActivated(false); + user.setActivationKey(UserUtils.generateActivationKey()); + user.setRoles(Collections.singleton(new UserRole(UserRoleConstants.USER))); + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + user = userRepository.save(user); + mailService.sendActivationEmail(user); + log.debug("Created Information for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + public UserDto activateUser(String activationKey) { + final User user = getUserByActivationKey(activationKey); + if (user == null) { + throw new UserActivationError(activationKey); + } + user.setActivated(true); + user.setActivationKey(null); + user.setActivationDate(null); + log.debug("Activated user: {}", user.getLogin()); + return userMapper.userEntityToUserDto(userRepository.save(user)); + } + + public UserDto updateUser(UserDto userDto) { + if (userDto.getId() == null) { + throw new EntityIdIsNullException(); + } + if (!Objects.equals( + Optional.ofNullable(getUserByEmail(userDto.getEmail())) + .map(BaseEntity::getId).orElse(userDto.getId()), + userDto.getId())) { + throw new UserEmailExistsException(userDto.getEmail()); + } + if (!Objects.equals( + Optional.ofNullable(getUserByLogin(userDto.getLogin())) + .map(BaseEntity::getId).orElse(userDto.getId()), + userDto.getId())) { + throw new UserLoginExistsException(userDto.getLogin()); + } + User user = userRepository.findOne(userDto.getId()); + if (user == null) { + throw new UserNotFoundException(userDto.getId().toString()); + } + if (applicationProperties.getUndeadUserLogin().equalsIgnoreCase(user.getLogin())) { + userDto.setLogin(applicationProperties.getUndeadUserLogin()); + userDto.setActivated(true); + userDto.setRoles(Collections.singletonList(new UserRoleDto(UserRoleConstants.ADMIN))); + } + user.setLogin(userDto.getLogin()); + user.setFirstName(userDto.getFirstName()); + user.setLastName(userDto.getLastName()); + user.setEmail(userDto.getEmail()); + if (userDto.isActivated() != user.getActivated()) { + if (userDto.isActivated()) { + user.setActivationKey(null); + user.setActivationDate(null); + } else { + user.setActivationKey(UserUtils.generateActivationKey()); + user.setActivationDate(new Date()); + } + } + user.setActivated(userDto.isActivated()); + final Set roles = userMapper.rolesFromDto(userDto.getRoles()); + user.setRoles(roles.isEmpty() + ? Collections.singleton(new UserRole(UserRoleConstants.USER)) + : roles); + if (!StringUtils.isEmpty(userDto.getOldPassword())) { + if (!userDto.isPasswordsValid() || !userDto.isOldPasswordValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + if (!passwordEncoder.matches(userDto.getOldPassword(), user.getPassword())) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + log.debug("Changed password for User: {}", user.getLogin()); + } + user = userRepository.save(user); + log.debug("Changed Information for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + public UserDto updateUserInformation(UserDto userDto) { + if (userDto.getId() == null) { + throw new EntityIdIsNullException(); + } + if (!Objects.equals( + Optional.ofNullable(getUserByEmail(userDto.getEmail())) + .map(BaseEntity::getId).orElse(userDto.getId()), + userDto.getId())) { + throw new UserEmailExistsException(userDto.getEmail()); + } + User user = userRepository.findOne(userDto.getId()); + if (user == null) { + throw new UserNotFoundException(userDto.getId().toString()); + } + user.setFirstName(userDto.getFirstName()); + user.setLastName(userDto.getLastName()); + user.setEmail(userDto.getEmail()); + user = userRepository.save(user); + log.debug("Updated Information for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + public UserDto changeUserPassword(UserDto userDto) { + if (userDto.getId() == null) { + throw new EntityIdIsNullException(); + } + if (!userDto.isPasswordsValid() || !userDto.isOldPasswordValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + final String login = UserUtils.getCurrentUserLogin(); + final User user = userRepository.findOneByLoginIgnoreCase(login); + if (user == null) { + throw new UserNotFoundException(login); + } + if (!passwordEncoder.matches(userDto.getOldPassword(), user.getPassword())) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + log.debug("Changed password for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(userRepository.save(user)); + } + + public boolean requestUserPasswordReset(String email) { + User user = userRepository.findOneByEmailIgnoreCase(email); + if (user == null) { + throw new UserNotFoundException(email); + } + if (!user.getActivated()) { + throw new UserNotActivatedException(); + } + user.setResetKey(UserUtils.generateResetKey()); + user.setResetDate(new Date()); + user = userRepository.save(user); + mailService.sendPasswordResetMail(user); + log.debug("Created Reset Password Request for User: {}", user.getLogin()); + return true; + } + + public boolean completeUserPasswordReset(String key, UserResetPasswordDto userResetPasswordDto) { + if (!userResetPasswordDto.isPasswordsValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + User user = userRepository.findOneByResetKey(key); + if (user == null) { + throw new UserResetKeyError(key); + } + user.setPassword(passwordEncoder.encode(userResetPasswordDto.getPassword())); + user.setResetKey(null); + user.setResetDate(null); + user = userRepository.save(user); + log.debug("Reset Password for User: {}", user.getLogin()); + return true; + } + + public UserDto deleteUser(Integer userId) { + final User user = userRepository.findOne(userId); + if (user == null) { + throw new UserNotFoundException(userId.toString()); + } + if (applicationProperties.getUndeadUserLogin().equalsIgnoreCase(user.getLogin())) { + throw new UserIsUndeadException(user.getLogin()); + } + userRepository.delete(user); + log.debug("Deleted User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + @Override + public UserDetails loadUserByUsername(String username) { + final User user = userRepository.findOneByLoginIgnoreCase(username); + if (user == null) { + throw new UserNotFoundException(username); + } + if (!user.getActivated()) { + throw new UserNotActivatedException(); + } + return new org.springframework.security.core.userdetails.User(user.getLogin(), + user.getPassword(), + Optional.ofNullable(user.getRoles()).orElse(Collections.emptySet()).stream() + .map(role -> new SimpleGrantedAuthority(role.getName())) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/ru/ulstu/user/service/UserSessionService.java b/src/main/java/ru/ulstu/user/service/UserSessionService.java new file mode 100644 index 0000000..36479a9 --- /dev/null +++ b/src/main/java/ru/ulstu/user/service/UserSessionService.java @@ -0,0 +1,57 @@ +package ru.ulstu.user.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.ulstu.core.model.response.PageableItems; +import ru.ulstu.core.jpa.OffsetablePageRequest; +import ru.ulstu.user.error.UserNotFoundException; +import ru.ulstu.user.model.User; +import ru.ulstu.user.model.UserSession; +import ru.ulstu.user.model.UserSessionListDto; +import ru.ulstu.user.repository.UserSessionRepository; + +import static ru.ulstu.core.util.StreamApiUtils.convert; + +@Service +@Transactional +public class UserSessionService { + private final Logger log = LoggerFactory.getLogger(UserSessionService.class); + private final UserSessionRepository userSessionRepository; + private final UserService userService; + + public UserSessionService(UserSessionRepository userSessionRepository, UserService userService) { + this.userSessionRepository = userSessionRepository; + this.userService = userService; + } + + @Transactional(readOnly = true) + public PageableItems getSessions(int offset, int count) { + final Page page = userSessionRepository.findAll( + new OffsetablePageRequest(offset, count, new Sort(Sort.Direction.DESC, "loginTime"))); + return new PageableItems<>(page.getTotalElements(), + convert(page.getContent(), UserSessionListDto::new)); + } + + public void createUserSession(String sessionId, String login, String ipAddress, String host) { + final User user = userService.getUserByLogin(login); + if (user == null) { + throw new UserNotFoundException(login); + } + userSessionRepository.save(new UserSession(sessionId, ipAddress, host, user)); + log.debug("User session {} created for user {}@{} ({})", sessionId, login, ipAddress, host); + } + + public void closeUserSession(String sessionId) { + final UserSession userSession = userSessionRepository.findOneBySessionId(sessionId); + if (userSession == null) { + throw new IllegalArgumentException(String.format("User session %s not found", sessionId)); + } + userSession.close(); + userSessionRepository.save(userSession); + log.debug("User session {} closed", sessionId); + } +} diff --git a/src/main/java/ru/ulstu/user/util/UserUtils.java b/src/main/java/ru/ulstu/user/util/UserUtils.java new file mode 100644 index 0000000..de585a5 --- /dev/null +++ b/src/main/java/ru/ulstu/user/util/UserUtils.java @@ -0,0 +1,35 @@ +package ru.ulstu.user.util; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public class UserUtils { + private static final int DEF_COUNT = 20; + + public static String generateActivationKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + + public static String generateResetKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + + public static String getCurrentUserLogin() { + final SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null) { + return null; + } + final Authentication authentication = securityContext.getAuthentication(); + if (authentication.getPrincipal() instanceof UserDetails) { + final UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); + return springSecurityUser.getUsername(); + } + if (authentication.getPrincipal() instanceof String) { + return (String) authentication.getPrincipal(); + } + return null; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8b7222a --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,36 @@ +# Server Settings +spring.main.banner-mode=off +server.port=8443 +server.http.port=8080 +spring.http.multipart.maxFileSize=20MB +spring.http.multipart.maxRequestSize=20MB +# Thymeleaf Settings +spring.thymeleaf.cache=false +# SSL Settings +security.require-ssl=true +server.ssl.key-store=classpath:sample.jks +server.ssl.key-store-password=secret +server.ssl.key-password=password +# Log settings (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF) +logging.level.ru.ulstu=DEBUG +# Mail Settings +spring.mail.host=smtp.yandex.ru +spring.mail.port=465 +spring.mail.username=balance@soft.kitchen +spring.mail.password=fkvfpbalance +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.ssl.enable=true +spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory +# JPA Settings +spring.datasource.url=jdbc:postgresql://localhost:5432/ng-tracker +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.driverclassName=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=validate +# Liquibase Settings +liquibase.drop-first=false +liquibase.enabled=true +liquibase.change-log=classpath:db/changelog-master.xml +# Application Settings +ng-tracker.base-url=https://127.0.0.1:8443 +ng-tracker.undead-user-login=admin \ No newline at end of file diff --git a/src/main/resources/commits.log b/src/main/resources/commits.log new file mode 100644 index 0000000..cb97503 --- /dev/null +++ b/src/main/resources/commits.log @@ -0,0 +1,489 @@ +Anton Romanov;Thu Mar 15 11:10:34 2018 +0400;change to date time +romanov73;Thu Mar 15 11:03:19 2018 +0400;read commits in constructor +romanov73;Thu Mar 15 09:12:07 2018 +0400;add commits page +Romanov Anton;Wed Mar 14 20:26:54 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 20:16:53 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 20:08:09 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 20:00:58 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 19:50:22 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 19:34:17 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 19:20:42 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:59:18 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:47:17 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:35:37 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:33:54 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:29:27 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:21:18 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:19:27 2018 +0000;Update README.md +Romanov Anton;Wed Mar 14 18:16:06 2018 +0000;Update README.md +romanov73;Wed Mar 14 21:28:10 2018 +0400;fix using constant +Aleksey Filippov;Wed Mar 14 20:07:25 2018 +0400;some fixes after merge +Aleksey Filippov;Wed Mar 14 19:52:45 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into odin-ui +Aleksey Filippov;Wed Mar 14 19:48:45 2018 +0400;refactoring of odin paginator +romanov73;Wed Mar 14 18:43:16 2018 +0400;Merge branch 'develop' into 36-rest +romanov73;Wed Mar 14 18:17:02 2018 +0400;Merge branch 'develop' into 36-rest +Aleksey Filippov;Wed Mar 14 18:11:00 2018 +0400;fix null value check in formatter +Aleksey Filippov;Wed Mar 14 18:06:16 2018 +0400;fix odin paginator style, fix odin negative file +romanov73;Wed Mar 14 18:04:47 2018 +0400;rename entities +Aleksey Filippov;Wed Mar 14 17:58:59 2018 +0400;improve odin table look +romanov73;Wed Mar 14 17:47:45 2018 +0400;fix db changelogs +romanov73;Wed Mar 14 17:47:20 2018 +0400;fix db changelogs +Gleb;Wed Mar 14 08:50:23 2018 +0000;Merge branch '48-' into 'develop' +funny73;Wed Mar 14 12:28:55 2018 +0400;Поправил отображение подразделения при редактировании смены +Aleksey Filippov;Wed Mar 14 00:02:24 2018 +0400;move navbar to the left, move odin css to separate file +Aleksey Filippov;Tue Mar 13 23:26:33 2018 +0400;some style fixes +Aleksey Filippov;Tue Mar 13 23:22:37 2018 +0400;move version panel to navbar +funny73;Tue Mar 13 20:41:41 2018 +0400;Поправил валидацию сменности +romanov73;Tue Mar 13 17:26:54 2018 +0400;fix offsetable page request +romanov73;Tue Mar 13 17:26:02 2018 +0400;fix table constructor +romanov73;Tue Mar 13 17:24:56 2018 +0400;fix checking empty value +Romanov Anton;Tue Mar 13 13:09:41 2018 +0000;Merge branch 'odin-ui' into '36-rest' +Aleksey Filippov;Tue Mar 13 16:59:27 2018 +0400;notes updated +Aleksey Filippov;Tue Mar 13 16:56:00 2018 +0400;odin refactoring +Aleksey Filippov;Tue Mar 13 16:53:38 2018 +0400;odin refactoring +romanov73;Tue Mar 13 15:27:17 2018 +0400;fix tool load calc +romanov73;Tue Mar 13 14:46:46 2018 +0400;fix area load calc +Aleksey Filippov;Tue Mar 13 13:38:23 2018 +0400;some fixes +romanov73;Tue Mar 13 13:33:25 2018 +0400;Merge remote-tracking branch 'origin/develop' into develop +romanov73;Tue Mar 13 13:33:07 2018 +0400;fix tools count calc +Aleksey Filippov;Tue Mar 13 13:12:09 2018 +0400;some odin refactoring +Aleksey Filippov;Tue Mar 13 13:11:41 2018 +0400;some offsetablepagerequest fix +Aleksey Filippov;Tue Mar 13 13:06:48 2018 +0400;add offsetablepagerequest +Aleksey Filippov;Tue Mar 13 07:27:12 2018 +0000;Merge branch '51-rest-points' into '36-rest' +romanov73;Tue Mar 13 11:22:02 2018 +0400;fix by comment: remove default version id +romanov73;Tue Mar 13 00:04:50 2018 +0400;fix path +romanov73;Mon Mar 12 23:55:05 2018 +0400;add tool square dictionary +romanov73;Mon Mar 12 23:36:49 2018 +0400;add dictionary pages +romanov73;Mon Mar 12 23:18:01 2018 +0400;sort second level menu items by name +romanov73;Mon Mar 12 23:11:22 2018 +0400;fix menu +romanov73;Mon Mar 12 22:52:15 2018 +0400;add stream api utils and converter for "map ... collect" +funny73;Mon Mar 12 20:01:47 2018 +0400;Добавил свойство базового изделия для изделия +romanov73;Mon Mar 12 19:46:52 2018 +0400;refactor units +romanov73;Mon Mar 12 19:36:57 2018 +0400;refactor tool types and work types +romanov73;Mon Mar 12 19:20:23 2018 +0400;refactor tools +romanov73;Mon Mar 12 19:07:09 2018 +0400;refactor stages +romanov73;Mon Mar 12 18:53:26 2018 +0400;refactor positions +romanov73;Mon Mar 12 18:27:36 2018 +0400;refactor employees +romanov73;Mon Mar 12 18:26:49 2018 +0400;refactor employees +romanov73;Mon Mar 12 18:09:38 2018 +0400;refactor categories +Aleksey Filippov;Mon Mar 12 17:32:06 2018 +0400;todo added +Aleksey Filippov;Mon Mar 12 17:29:51 2018 +0400;paginator added to odin +Aleksey Filippov;Mon Mar 12 17:29:06 2018 +0400;notes updated +funny73;Mon Mar 12 16:29:13 2018 +0400;Добавил сущность изделие без привязки к производственной программе +funny73;Mon Mar 12 15:16:35 2018 +0400;Переименовал сущность Product на ProductOnProgram Ещё немного переименования +funny73;Mon Mar 12 14:39:52 2018 +0400;Переименовал сущность Product на ProductOnProgram +Aleksey Filippov;Mon Mar 12 13:44:42 2018 +0400;add formatters and initial form support to odin +Aleksey Filippov;Mon Mar 12 13:44:16 2018 +0400;userlistdto refactoring +Aleksey Filippov;Mon Mar 12 13:43:59 2018 +0400;dateutils improvements +Aleksey Filippov;Mon Mar 12 13:43:43 2018 +0400;add support of localdatetime type to odin +Aleksey Filippov;Mon Mar 12 13:42:54 2018 +0400;odin backend example added +Aleksey Filippov;Mon Mar 12 13:42:12 2018 +0400;notes updated +funny73;Mon Mar 12 12:16:02 2018 +0400;Поправил всплывающие сообщения при работе с "Категориями" +Aleksey Filippov;Mon Mar 12 11:25:51 2018 +0400;some refactoring +Aleksey Filippov;Mon Mar 12 11:25:30 2018 +0400;odinid annotation added +Aleksey Filippov;Sun Mar 11 15:23:49 2018 +0400;notes updated +Aleksey Filippov;Sun Mar 11 15:18:45 2018 +0400;some fixes +Aleksey Filippov;Sun Mar 11 15:09:57 2018 +0400;simple table draw support added, some refactoring +Aleksey Filippov;Sun Mar 11 15:08:22 2018 +0400;add user id field to userlistdto +Aleksey Filippov;Sun Mar 11 14:32:50 2018 +0400;add jsonproperty annotation support +Aleksey Filippov;Sun Mar 11 13:38:45 2018 +0400;some template fixes +Aleksey Filippov;Sun Mar 11 13:33:09 2018 +0400;some balance page fixes +Aleksey Filippov;Sun Mar 11 13:25:58 2018 +0400;add jsonignore annotation support +Aleksey Filippov;Thu Mar 8 14:16:20 2018 +0400;some odin improvements +Aleksey Filippov;Wed Mar 7 16:03:04 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Wed Mar 7 16:01:21 2018 +0400;odin submodule for basic types added, some refactoring +romanov73;Tue Mar 6 22:52:02 2018 +0400;remove old packages +romanov73;Tue Mar 6 19:12:27 2018 +0400;add tool type crud +romanov73;Tue Mar 6 13:09:51 2018 +0400;add version crud +romanov73;Tue Mar 6 10:48:36 2018 +0400;add unit crud +romanov73;Mon Mar 5 16:58:37 2018 +0400;fix tree +romanov73;Mon Mar 5 16:38:37 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +romanov73;Mon Mar 5 16:38:21 2018 +0400;employee crud +Aleksey Filippov;Mon Mar 5 15:43:49 2018 +0400;some fixes +Aleksey Filippov;Mon Mar 5 15:39:53 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Mon Mar 5 15:39:33 2018 +0400;user reset password function added, some refactoring +romanov73;Mon Mar 5 15:29:16 2018 +0400;fix delete category +Aleksey Filippov;Mon Mar 5 15:08:27 2018 +0400;user change password function added, some refactoring +romanov73;Mon Mar 5 15:08:17 2018 +0400;add category crud +romanov73;Mon Mar 5 14:07:10 2018 +0400;add tree component +Aleksey Filippov;Mon Mar 5 11:30:22 2018 +0400;user delete function added, some refactoring +Aleksey Filippov;Mon Mar 5 11:18:23 2018 +0400;user update function added +Aleksey Filippov;Mon Mar 5 11:18:06 2018 +0400;some refactoring +Aleksey Filippov;Mon Mar 5 10:06:04 2018 +0400;add user activation function +Aleksey Filippov;Mon Mar 5 09:48:22 2018 +0400;add scheduler for users +Aleksey Filippov;Mon Mar 5 09:48:07 2018 +0400;add activateddate field to userentity +Aleksey Filippov;Mon Mar 5 09:47:25 2018 +0400;notes file updated +Aleksey Filippov;Mon Mar 5 09:47:11 2018 +0400;some refactoring +Aleksey Filippov;Mon Mar 5 09:21:49 2018 +0400;thymeleaf cache settings added +Aleksey Filippov;Mon Mar 5 09:21:11 2018 +0400;notes file added +Aleksey Filippov;Mon Mar 5 09:20:57 2018 +0400;some refactoring +Aleksey Filippov;Mon Mar 5 08:37:01 2018 +0400;some refactoring +romanov73;Mon Mar 5 00:08:02 2018 +0400;add callbacks on version getters +romanov73;Mon Mar 5 00:07:34 2018 +0400;fix npe +romanov73;Sun Mar 4 01:13:57 2018 +0400;fix column style +romanov73;Sun Mar 4 01:06:38 2018 +0400;fix table style +romanov73;Sun Mar 4 00:27:14 2018 +0400;add balance dto + draw employee balance +romanov73;Sat Mar 3 22:43:06 2018 +0400;add balance services +romanov73;Sat Mar 3 16:31:19 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +romanov73;Sat Mar 3 16:31:04 2018 +0400;add employee balance table +Aleksey Filippov;Sat Mar 3 15:54:23 2018 +0400;some user support improvements +Aleksey Filippov;Sat Mar 3 15:53:20 2018 +0400;edit migration files, need manual fix of databasechangelog table +Aleksey Filippov;Sat Mar 3 15:51:58 2018 +0400;add application properties handler +Aleksey Filippov;Sat Mar 3 15:51:19 2018 +0400;disable tests +Aleksey Filippov;Sat Mar 3 13:49:32 2018 +0400;some refactoring +romanov73;Fri Mar 2 18:10:23 2018 +0400;add balance page +Aleksey Filippov;Fri Mar 2 18:04:39 2018 +0400;some refactoring +Aleksey Filippov;Fri Mar 2 17:51:41 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Fri Mar 2 17:50:59 2018 +0400;advicecontroller improvements +Aleksey Filippov;Fri Mar 2 17:50:39 2018 +0400;some mvc improvements +romanov73;Fri Mar 2 15:15:46 2018 +0400;add dtos +romanov73;Fri Mar 2 15:02:38 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Fri Mar 2 14:54:33 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Fri Mar 2 14:53:39 2018 +0400;add migrations +Aleksey Filippov;Fri Mar 2 14:53:30 2018 +0400;some core improvements +Aleksey Filippov;Fri Mar 2 14:53:13 2018 +0400;some users improvements +romanov73;Fri Mar 2 12:42:54 2018 +0400;add category service +romanov73;Fri Mar 2 12:37:51 2018 +0400;fix unit service +romanov73;Fri Mar 2 11:24:11 2018 +0400;add unit controller +romanov73;Thu Mar 1 23:19:42 2018 +0400;fix versions panel +romanov73;Thu Mar 1 22:50:57 2018 +0400;Merge branch 'develop' into 36-rest +romanov73;Thu Mar 1 22:50:37 2018 +0400;Merge branch 'develop' into 36-rest +Romanov Anton;Thu Mar 1 18:31:03 2018 +0000;Merge branch '29-' into 'develop' +romanov73;Thu Mar 1 22:25:49 2018 +0400;change versions +romanov73;Thu Mar 1 20:14:22 2018 +0400;show version select +romanov73;Thu Mar 1 19:56:24 2018 +0400;add old models, add versions controller +romanov73;Thu Mar 1 19:55:59 2018 +0400;add old models, add versions controller +romanov73;Thu Mar 1 19:04:47 2018 +0400;save menu to session +romanov73;Thu Mar 1 18:50:00 2018 +0400;add favicon +funny73;Thu Mar 1 18:01:43 2018 +0400;1) Поправил имена колонок и таблицы для смен под постгрес 2) Исправил ошибку в имени переменной thidShift ->thirdShift +romanov73;Thu Mar 1 16:03:03 2018 +0400;add menu and restore changelogs +funny73;Thu Mar 1 15:33:36 2018 +0400;Добавил распорядок смен для каждого подразделения. Смена назначается на месяц. Есть возможность сохранить любой вариант комбинирования 1,2 и 3 смены. Например (1,3); (2); ()... Добавлен интерфейс для просмотра и редактирования распорядка смен по каждому подразделению. Добавлена валидация - для каждого месяца может быть только один распорядок смен. +Aleksey Filippov;Thu Mar 1 15:23:06 2018 +0400;migrate to webjars +Aleksey Filippov;Thu Mar 1 13:31:16 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Thu Mar 1 13:30:55 2018 +0400;create migrations for user models +Aleksey Filippov;Thu Mar 1 13:30:09 2018 +0400;old code refactoring +romanov73;Thu Mar 1 12:51:14 2018 +0400;add basic menu from rest +romanov73;Thu Mar 1 11:51:17 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +romanov73;Thu Mar 1 11:50:59 2018 +0400;try to fix ci: 3 remove tests +Aleksey Filippov;Thu Mar 1 11:50:25 2018 +0400;Merge remote-tracking branch 'origin/36-rest' into 36-rest +Aleksey Filippov;Thu Mar 1 11:49:59 2018 +0400;moved to new database +romanov73;Thu Mar 1 11:45:40 2018 +0400;try to fix ci: 2 change image +romanov73;Thu Mar 1 11:44:42 2018 +0400;try to fix ci: 1 +romanov73;Thu Mar 1 11:15:52 2018 +0400;fix csrf tokens +Aleksey Filippov;Thu Mar 1 00:25:43 2018 +0400;initial user management support added +romanov73;Wed Feb 28 22:26:00 2018 +0400;save current version in local storage +romanov73;Wed Feb 28 18:40:57 2018 +0400;add ajax datatable +romanov73;Wed Feb 28 18:01:23 2018 +0400;add static index page with menu +Romanov Anton;Tue Feb 27 16:53:04 2018 +0000;Merge branch '33-' into 'develop' +romanov73;Tue Feb 27 20:50:44 2018 +0400;calc area balance +romanov73;Tue Feb 27 17:36:31 2018 +0400;remove unused panel +Romanov Anton;Tue Feb 27 13:06:02 2018 +0000;Merge branch '32-' into 'develop' +romanov73;Tue Feb 27 17:02:35 2018 +0400;calc tool balance +romanov73;Tue Feb 27 15:45:39 2018 +0400;calc tool balance +romanov73;Tue Feb 27 12:16:41 2018 +0400;calc tool power +romanov73;Tue Feb 27 11:40:59 2018 +0400;use tool types in dto +romanov73;Tue Feb 27 11:29:05 2018 +0400;fix dtos +romanov73;Mon Feb 26 23:50:53 2018 +0400;inherit dtos +romanov73;Mon Feb 26 15:38:00 2018 +0400;fix services for balance calculation +Aleksey Filippov;Mon Feb 26 15:00:48 2018 +0400;move from maven to gradle, move from javaee to spring boot +romanov73;Thu Feb 22 18:03:35 2018 +0400;Add menu resource +Romanov Anton;Tue Feb 20 18:12:55 2018 +0000;Merge branch 'balance-different-dto' into 'develop' +romanov73;Tue Feb 20 22:09:33 2018 +0400;restore tests +romanov73;Tue Feb 20 21:46:45 2018 +0400;filter employees by stage +romanov73;Tue Feb 20 21:21:27 2018 +0400;filter employees by workload +romanov73;Tue Feb 20 20:58:23 2018 +0400;Merge branch 'develop' into balance-different-dto +funny73;Tue Feb 20 19:52:24 2018 +0400;Add shift unit model Add link to unit view +romanov73;Tue Feb 20 16:14:09 2018 +0400;Important fix using coefficient +romanov73;Mon Feb 19 19:00:07 2018 +0400;partially fix ajustment +romanov73;Mon Feb 19 18:24:10 2018 +0400;Modify dto for using with different periods and work types +Romanov Anton;Mon Feb 19 08:11:39 2018 +0000;Merge branch '30-' into 'develop' +romanov73;Mon Feb 19 12:09:05 2018 +0400;Add work type to position +Romanov Anton;Wed Feb 14 20:57:35 2018 +0000;Merge branch '34-' into 'develop' +romanov73;Thu Feb 15 00:51:24 2018 +0400;fixes by comments +romanov73;Thu Feb 15 00:47:21 2018 +0400;merge fields +romanov73;Wed Feb 14 23:42:35 2018 +0400;Merge branch 'develop' into 34-gleb +romanov73;Wed Feb 14 21:05:51 2018 +0400;add clock +Romanov Anton;Wed Feb 14 16:49:29 2018 +0000;Merge branch '45-balance-employee-recomendations' into 'develop' +romanov73;Wed Feb 14 20:46:59 2018 +0400;cleanup code +romanov73;Wed Feb 14 18:25:42 2018 +0400;reduce code +romanov73;Wed Feb 14 18:24:59 2018 +0400;adjust additional employees +romanov73;Wed Feb 14 17:22:10 2018 +0400;move employees from other units +romanov73;Tue Feb 13 23:59:09 2018 +0400;fix save product name +romanov73;Tue Feb 13 23:28:30 2018 +0400;add converter, fix UI and backing for add complex object +romanov73;Tue Feb 13 22:10:59 2018 +0400;Merge branch 'develop' into 34-gleb +funny73;Tue Feb 13 18:07:25 2018 +0400;Add stage to workload +funny73;Tue Feb 13 16:05:45 2018 +0400;Add stage to category +romanov73;Tue Feb 13 13:20:16 2018 +0400;add map of all units balances +romanov73;Tue Feb 13 11:24:50 2018 +0400;move method +romanov73;Tue Feb 13 00:35:30 2018 +0400;add dialog +romanov73;Mon Feb 12 22:59:49 2018 +0400;filter only workshops and manufactures +romanov73;Mon Feb 12 22:04:55 2018 +0400;rename employees +romanov73;Mon Feb 12 17:34:37 2018 +0400;fix font size +romanov73;Mon Feb 12 16:59:16 2018 +0400;fix versions panel width +Romanov Anton;Mon Feb 12 12:47:04 2018 +0000;Merge branch '41-' into 'develop' +romanov73;Mon Feb 12 16:43:06 2018 +0400;change to working hours +romanov73;Mon Feb 12 14:05:36 2018 +0400;remove constructor +romanov73;Mon Feb 12 14:05:13 2018 +0400;Merge remote-tracking branch 'origin/develop' into develop +romanov73;Mon Feb 12 14:04:57 2018 +0400;fix selects +Romanov Anton;Mon Feb 12 09:20:05 2018 +0000;Merge branch '39-work-type-code' into 'develop' +Aleksey Filippov;Mon Feb 12 13:16:36 2018 +0400;add migration for work type code +Aleksey Filippov;Mon Feb 12 13:16:28 2018 +0400;add work type code support to xhtml +Aleksey Filippov;Mon Feb 12 13:16:12 2018 +0400;add work type code +Romanov Anton;Mon Feb 12 08:25:47 2018 +0000;Update README.md +Romanov Anton;Mon Feb 12 08:25:35 2018 +0000;Update README.md +Romanov Anton;Mon Feb 12 08:07:30 2018 +0000;Update README.md +Romanov Anton;Mon Feb 12 07:10:38 2018 +0000;Merge branch '40-' into 'develop' +Aleksey Filippov;Mon Feb 12 11:08:13 2018 +0400;add migration for position type +Aleksey Filippov;Mon Feb 12 11:07:45 2018 +0400;add position type support to xhtml +Aleksey Filippov;Mon Feb 12 11:07:23 2018 +0400;move position converter from boundary to view +Aleksey Filippov;Mon Feb 12 11:06:55 2018 +0400;add position type converter +Aleksey Filippov;Mon Feb 12 11:06:35 2018 +0400;add position type to backing +Aleksey Filippov;Mon Feb 12 11:06:00 2018 +0400;add position type support to service +Aleksey Filippov;Mon Feb 12 11:05:44 2018 +0400;add position type to model +Romanov Anton;Mon Feb 12 07:02:32 2018 +0000;Merge branch '33-' into 'develop' +romanov73;Mon Feb 12 14:58:51 2018 +0400;refactor +romanov73;Mon Feb 12 14:58:33 2018 +0400;add human hours coefficient +romanov73;Mon Feb 12 14:56:43 2018 +0400;fix api name +romanov73;Mon Feb 12 00:58:15 2018 +0400;add balance button +romanov73;Sun Feb 11 01:36:15 2018 +0400;add tools balance prototype +romanov73;Sat Feb 10 23:09:33 2018 +0400;move enum +romanov73;Sat Feb 10 23:02:36 2018 +0400;refactor +romanov73;Sat Feb 10 22:54:28 2018 +0400;add quarter +romanov73;Sat Feb 10 22:42:14 2018 +0400;refactor +romanov73;Sat Feb 10 20:52:46 2018 +0400;Merge branch 'develop' into 33-balance-area +romanov73;Sat Feb 10 20:52:32 2018 +0400;Merge branch 'develop' into 33-balance-area +Romanov Anton;Sat Feb 10 16:14:26 2018 +0000;Merge branch '31-map-2-dto' into 'develop' +romanov73;Sat Feb 10 19:35:17 2018 +0400;fixes after Almaz consultation +romanov73;Sat Feb 10 14:25:00 2018 +0400;fix npe +romanov73;Sat Feb 10 13:50:21 2018 +0400;add filter by work type +romanov73;Sat Feb 10 13:08:56 2018 +0400;rename field +romanov73;Sat Feb 10 13:08:38 2018 +0400;filter by all children +Aleksey Filippov;Sat Feb 10 11:26:17 2018 +0400;some fixes +Aleksey Filippov;Sat Feb 10 11:26:06 2018 +0400;add dto for total balance +Aleksey Filippov;Sat Feb 10 11:11:58 2018 +0400;add dto for employee load by unit +Aleksey Filippov;Sat Feb 10 10:30:11 2018 +0400;getEmployeesByUnit method refactoring +romanov73;Sat Feb 10 00:22:37 2018 +0400;add other type of balance area +romanov73;Fri Feb 9 22:03:39 2018 +0400;add tools balance +romanov73;Fri Feb 9 19:50:21 2018 +0400;divide balance page +Aleksey Filippov;Fri Feb 9 16:37:18 2018 +0400;adapt backing and view to areas and employee experience dtos +Aleksey Filippov;Fri Feb 9 16:36:18 2018 +0400;add dto for employee experience +Aleksey Filippov;Fri Feb 9 16:35:51 2018 +0400;add dto for areas +Aleksey Filippov;Fri Feb 9 14:57:01 2018 +0400;some ui fixes +Aleksey Filippov;Fri Feb 9 14:55:36 2018 +0400;move db methods from getters to init +romanov73;Thu Feb 8 01:20:12 2018 +0400;add areas panel +romanov73;Thu Feb 8 01:19:13 2018 +0400;add areas panel +romanov73;Thu Feb 8 01:02:39 2018 +0400;add global preloader +romanov73;Thu Feb 8 00:57:46 2018 +0400;select default unit +romanov73;Thu Feb 8 00:57:23 2018 +0400;add preloader +romanov73;Wed Feb 7 22:46:44 2018 +0400;change id +Romanov Anton;Wed Feb 7 13:53:35 2018 +0000;Merge branch '23-' into 'develop' +Romanov Anton;Wed Feb 7 13:49:27 2018 +0000;Merge branch 'deploy-fixes' into 'develop' +funny73;Wed Feb 7 17:34:41 2018 +0400;UDP modify AirplaneKitCounter to double +Aleksey Filippov;Wed Feb 7 17:33:23 2018 +0400;some wildfly deploy fixes +funny73;Wed Feb 7 17:19:22 2018 +0400;Modify AirplaneKitCounter to double -> Workload.java +funny73;Wed Feb 7 17:00:10 2018 +0400;Add AirplaneKitCounter to balance view -> BalanceEmployeeService.java Modify Worckload total Value (Value * AirplaneKitCounter) -> BalanceService.java +funny73;Wed Feb 7 14:13:35 2018 +0400;Add AirplaneKitCounter to view +funny73;Wed Feb 7 14:13:15 2018 +0400;Add AirplaneKitCounter to model Workload +funny73;Wed Feb 7 14:12:15 2018 +0400;Changelog for airplainetKitCounter +Romanov Anton;Wed Feb 7 06:54:55 2018 +0000;Merge branch '22-' into 'develop' +romanov73;Wed Feb 7 14:47:34 2018 +0400;add unit types +Romanov Anton;Tue Feb 6 22:43:40 2018 +0000;Merge branch '25-' into 'develop' +romanov73;Wed Feb 7 02:41:34 2018 +0400;filter balance by unit +romanov73;Wed Feb 7 02:06:46 2018 +0400;filter balance by unit +romanov73;Wed Feb 7 02:05:10 2018 +0400;filter balance by unit +romanov73;Wed Feb 7 00:41:01 2018 +0400;add method get all unit children +romanov73;Tue Feb 6 23:26:18 2018 +0400;add filter by unit in balance results +romanov73;Tue Feb 6 21:26:45 2018 +0400;refactor work with tree of menu items +romanov73;Tue Feb 6 21:08:01 2018 +0400;refactor work with tree +romanov73;Tue Feb 6 21:07:22 2018 +0400;fix update after remove enum key +romanov73;Tue Feb 6 20:03:31 2018 +0400;rename method +Gleb;Tue Feb 6 15:07:04 2018 +0000;Merge branch '17-' into 'develop' +romanov73;Tue Feb 6 21:15:12 2018 +0400;fix inf. +funny73;Tue Feb 6 19:02:36 2018 +0400;Add changelog for units with unit_type == 'Корпус', unit_type='Цех' +funny73;Tue Feb 6 18:19:50 2018 +0400;Remove "Korpus" from "Tip podrazdeleniya" +Romanov Anton;Tue Feb 6 11:31:43 2018 +0000;Merge branch '24-' into 'develop' +romanov73;Tue Feb 6 19:01:21 2018 +0400;add user +Romanov Anton;Thu Feb 1 19:45:21 2018 +0000;Merge branch 'wildfly-deploy' into 'develop' +Aleksey Filippov;Thu Feb 1 23:24:34 2018 +0400;add wildfly-maven-plugin, remove maven-glassfish-plugin +romanov73;Thu Feb 1 19:51:24 2018 +0400;add todos +Romanov Anton;Wed Jan 31 15:03:39 2018 +0000;Merge branch 'master' into 'develop' +romanov73;Wed Jan 31 18:53:27 2018 +0400;fix round +romanov73;Wed Jan 31 18:43:33 2018 +0400;fix round +romanov73;Wed Jan 31 18:38:33 2018 +0400;add balance by employees +romanov73;Wed Jan 31 18:30:18 2018 +0400;add balance by employees +Romanov Anton;Wed Jan 31 13:02:52 2018 +0000;Merge branch 'develop' into 'master' +Romanov Anton;Wed Jan 31 12:58:00 2018 +0000;Merge branch '4-' into 'develop' +romanov73;Wed Jan 31 16:47:51 2018 +0400;add balance by employees +romanov73;Wed Jan 31 00:47:39 2018 +0400;add calculation employee loads +romanov73;Wed Jan 31 00:47:25 2018 +0400;add calculation employee loads +romanov73;Wed Jan 31 00:47:08 2018 +0400;add calculation employee loads +romanov73;Wed Jan 31 00:46:53 2018 +0400;add calculation employee loads +romanov73;Wed Jan 31 00:45:59 2018 +0400;add interface for edit additional fields +romanov73;Wed Jan 31 00:45:04 2018 +0400;add comparable for using in tree map +romanov73;Wed Jan 31 00:44:33 2018 +0400;add fields for production program +romanov73;Wed Jan 31 00:43:56 2018 +0400;move interface to base entity +romanov73;Wed Jan 31 00:43:21 2018 +0400;add database fields for production program +romanov73;Wed Jan 31 00:42:55 2018 +0400;add dynamic columns +romanov73;Tue Jan 30 15:11:12 2018 +0400;add work type for workload +romanov73;Tue Jan 30 14:55:56 2018 +0400;add comments +romanov73;Tue Jan 30 03:00:28 2018 +0400;show by production program +romanov73;Tue Jan 30 00:07:31 2018 +0400;add employee available capacity +romanov73;Mon Jan 29 23:42:06 2018 +0400;fix units hierarchy bypass +romanov73;Mon Jan 29 23:28:28 2018 +0400;divide logic +romanov73;Mon Jan 29 22:59:45 2018 +0400;add employee experience table +romanov73;Mon Jan 29 22:15:29 2018 +0400;fix calc areas +romanov73;Mon Jan 29 21:57:23 2018 +0400;fix calc employee experience +romanov73;Mon Jan 29 21:12:52 2018 +0400;calc employee experience by units +romanov73;Mon Jan 29 14:01:36 2018 +0400;add employee experience +Romanov Anton;Mon Jan 29 06:04:01 2018 +0000;Update README.md +Romanov Anton;Sun Jan 28 16:16:56 2018 +0000;Merge branch 'develop' into 'master' +Romanov Anton;Sun Jan 28 16:06:14 2018 +0000;Merge branch '3-' into 'develop' +romanov73;Sun Jan 28 19:53:21 2018 +0400;remove product work types edit +romanov73;Sun Jan 28 19:41:31 2018 +0400;fix months select +romanov73;Sun Jan 28 19:06:10 2018 +0400;fix year select +romanov73;Sun Jan 28 04:28:21 2018 +0400;fix program edit +romanov73;Sun Jan 28 04:27:55 2018 +0400;add product backend +romanov73;Sun Jan 28 04:26:53 2018 +0400;add table and fields +Romanov Anton;Sat Jan 27 08:11:15 2018 +0000;Merge branch '9-' into 'develop' +romanov73;Sat Jan 27 15:28:22 2018 +0400;sort menu items, change logo +Romanov Anton;Sat Jan 27 05:55:52 2018 +0000;Update README.md +romanov73;Sat Jan 20 00:12:44 2018 +0400;fix versions select +Romanov Anton;Fri Jan 19 04:27:48 2018 +0000;Merge branch 'tool-square-catalog' into 'master' +Aleksey Filippov;Fri Jan 19 01:11:50 2018 +0400;add tool square catalog migration +Aleksey Filippov;Thu Jan 18 02:20:48 2018 +0400;add tool square catalog +romanov73;Thu Jan 11 21:22:14 2018 +0400;fix rest path +romanov73;Tue Jan 9 03:44:10 2018 +0400;fixes +romanov73;Tue Jan 9 00:09:27 2018 +0400;fix unit name +romanov73;Tue Jan 9 00:07:47 2018 +0400;fix year +romanov73;Mon Jan 8 19:16:28 2018 +0400;add short view +romanov73;Mon Jan 8 11:55:30 2018 +0400;fix +romanov73;Mon Jan 8 01:24:37 2018 +0400;fill work type by tool name +romanov73;Mon Jan 8 00:23:00 2018 +0400;ui fixes +romanov73;Sun Jan 7 23:54:40 2018 +0400;fix units hierarchy, add unit type, calc areas balance +romanov73;Sat Jan 6 22:51:11 2018 +0400;remove versions +romanov73;Sat Jan 6 22:06:03 2018 +0400;fix edit program +romanov73;Sat Jan 6 21:40:11 2018 +0400;add title image +romanov73;Sat Jan 6 18:04:27 2018 +0400;add production program input +romanov73;Wed Jan 3 21:05:48 2018 +0400;change wizard to tabs +romanov73;Wed Jan 3 01:03:05 2018 +0400;add wizard +romanov73;Sat Dec 30 02:02:42 2017 +0400;fix tools loading +romanov73;Sat Dec 30 01:46:01 2017 +0400;fix tools loading +romanov73;Sat Dec 30 01:32:33 2017 +0400;fix tools loading +romanov73;Sat Dec 30 01:28:24 2017 +0400;fix tools loading +romanov73;Sat Dec 30 00:54:26 2017 +0400;fix employee loading +romanov73;Fri Dec 29 19:04:14 2017 +0400;fix unit select +romanov73;Fri Dec 29 17:19:10 2017 +0400;fix unit select +romanov73;Fri Dec 29 01:40:38 2017 +0400;add cache +romanov73;Fri Dec 29 01:27:18 2017 +0400;add cache +romanov73;Fri Dec 29 01:14:09 2017 +0400;add cache +romanov73;Fri Dec 29 01:12:17 2017 +0400;change color +romanov73;Fri Dec 29 00:50:57 2017 +0400;add ci +romanov73;Fri Dec 29 00:48:10 2017 +0400;add ci +romanov73;Fri Dec 29 00:34:36 2017 +0400;add ci +romanov73;Fri Dec 29 00:31:22 2017 +0400;add ci +romanov73;Fri Dec 29 00:18:51 2017 +0400;add ci +romanov73;Fri Dec 29 00:16:44 2017 +0400;add ci +romanov73;Fri Dec 29 00:13:33 2017 +0400;add ci +romanov73;Fri Dec 29 00:09:51 2017 +0400;fix menus +Romanov Anton;Thu Dec 28 19:47:02 2017 +0000;Merge branch 'balance-example' into 'master' +romanov73;Thu Dec 28 23:44:52 2017 +0400;add other dictionaries +romanov73;Sat Dec 23 10:40:41 2017 +0400;add tool and work types +romanov73;Sat Dec 23 09:12:38 2017 +0400;fix menu item +romanov73;Sat Dec 23 09:12:23 2017 +0400;fix employee load +romanov73;Sat Dec 23 08:51:18 2017 +0400;add employee category +romanov73;Sat Dec 23 08:22:10 2017 +0400;add categories dictionary +romanov73;Fri Dec 22 12:38:08 2017 +0400;add menu item +romanov73;Fri Dec 22 12:31:26 2017 +0400;add employee category +romanov73;Fri Dec 22 09:55:31 2017 +0400;fix calendar +romanov73;Fri Dec 22 09:33:56 2017 +0400;fix date +romanov73;Thu Dec 21 16:38:20 2017 +0400;fix for context path change +romanov73;Thu Dec 21 13:58:02 2017 +0400;fluid panel +romanov73;Wed Dec 20 20:50:17 2017 +0400;fix context path +romanov73;Wed Dec 20 17:12:19 2017 +0400;fix displaying position +romanov73;Wed Dec 20 17:10:14 2017 +0400;load employees from file +romanov73;Wed Dec 20 15:09:54 2017 +0400;fix tool loading +romanov73;Mon Dec 18 18:18:04 2017 +0400;add resource versions +romanov73;Mon Dec 18 17:36:50 2017 +0400;add resource +romanov73;Mon Dec 18 17:07:47 2017 +0400;add swagger +romanov73;Sat Dec 16 09:42:46 2017 +0400;add new version +romanov73;Fri Dec 15 19:50:46 2017 +0400;fix title +romanov73;Fri Dec 15 19:24:13 2017 +0400;fix title +romanov73;Fri Dec 15 11:57:18 2017 +0400;add menu items +romanov73;Fri Dec 15 11:52:01 2017 +0400;add positions dictionary +romanov73;Fri Dec 15 11:29:02 2017 +0400;fix select for employee +romanov73;Fri Dec 15 10:35:12 2017 +0400;add employee backing +romanov73;Fri Dec 15 09:36:33 2017 +0400;fix 500 error page +romanov73;Thu Dec 14 22:26:45 2017 +0400;fix mapping +romanov73;Thu Dec 14 22:49:44 2017 +0400;add employee +romanov73;Thu Dec 14 22:37:30 2017 +0400;add employee +romanov73;Thu Dec 14 14:12:03 2017 +0400;fix crud service +romanov73;Thu Dec 14 12:51:59 2017 +0400;fix table style +romanov73;Thu Dec 14 12:22:48 2017 +0400;remove border for grid +romanov73;Thu Dec 14 12:20:14 2017 +0400;add version_id for units +romanov73;Thu Dec 14 11:49:53 2017 +0400;fix font size +romanov73;Thu Dec 14 11:08:46 2017 +0400;add unit fields +romanov73;Thu Dec 14 10:23:01 2017 +0400;KISS units hierarchy +romanov73;Wed Dec 13 19:04:05 2017 +0400;refresh page +romanov73;Wed Dec 13 17:43:54 2017 +0400;add version +romanov73;Wed Dec 13 01:46:05 2017 +0400;body width +romanov73;Wed Dec 13 01:45:17 2017 +0400;add menu item +romanov73;Tue Dec 12 22:20:07 2017 +0400;fix tree +romanov73;Tue Dec 12 20:02:20 2017 +0400;fix login +romanov73;Tue Dec 12 19:29:03 2017 +0400;add unit tree +romanov73;Tue Dec 12 18:23:38 2017 +0400;add unit dictionary +romanov73;Tue Dec 12 16:18:03 2017 +0400;add filter +romanov73;Tue Dec 12 14:13:31 2017 +0400;fix menu session +romanov73;Tue Dec 12 00:10:47 2017 +0400;remove bootstrap +romanov73;Mon Dec 11 21:53:18 2017 +0400;add unit select +romanov73;Mon Dec 11 19:49:15 2017 +0400;add menu item and role +romanov73;Mon Dec 11 19:37:38 2017 +0400;fix permissions +romanov73;Mon Dec 11 17:08:39 2017 +0400;refactor and add tools dictionary backing +romanov73;Mon Dec 11 17:08:04 2017 +0400;refactor and add tools dictionary backing +romanov73;Mon Dec 11 17:07:27 2017 +0400;refactor and add tools dictionary backing +romanov73;Mon Dec 11 14:12:24 2017 +0400;add tools dictionary page +romanov73;Sat Dec 9 09:54:45 2017 +0400;add global exception hadler +romanov73;Sat Nov 25 13:52:33 2017 +0400;add monitoring +romanov73;Sat Nov 25 13:21:18 2017 +0400;fix xls and xlsx +romanov73;Sat Nov 25 12:39:45 2017 +0400;upload tools +romanov73;Fri Nov 24 22:16:27 2017 +0400;save tools +romanov73;Fri Nov 24 13:06:21 2017 +0400;fix message +romanov73;Fri Nov 24 12:11:43 2017 +0400;add loading from xlsx +romanov73;Fri Nov 17 19:43:35 2017 +0400;add message +romanov73;Fri Nov 17 15:11:22 2017 +0400;fix button +romanov73;Fri Nov 17 10:15:42 2017 +0400;add registration service +romanov73;Thu Nov 16 21:35:04 2017 +0400;add push script +romanov73;Thu Nov 16 20:54:49 2017 +0400;fix named query execution +romanov73;Sun Nov 12 22:19:03 2017 +0400;sort menu items +romanov73;Sat Nov 11 15:09:35 2017 +0400;fix title +romanov73;Sat Nov 11 14:54:25 2017 +0400;fix table +romanov73;Sat Nov 11 14:35:38 2017 +0400;fix style attribute +romanov73;Sat Nov 11 14:08:56 2017 +0400;show user sessions +romanov73;Sat Nov 11 13:51:34 2017 +0400;rename service +romanov73;Fri Nov 10 22:03:25 2017 +0400;fix update tree +romanov73;Fri Nov 10 14:29:25 2017 +0400;edit menu items +romanov73;Fri Nov 10 12:17:24 2017 +0400;set bootstrap theme for primefaces +romanov73;Thu Nov 9 23:40:40 2017 +0400;fix saving entitites +romanov73;Thu Nov 9 23:27:42 2017 +0400;fix saving entitites +romanov73;Thu Nov 9 20:51:19 2017 +0400;fix titles +romanov73;Thu Nov 9 18:00:45 2017 +0400;fix login page title +romanov73;Thu Nov 9 13:36:21 2017 +0400;commit log +romanov73;Thu Nov 9 07:31:37 2017 +0400;reverse sort commits +romanov73;Thu Nov 9 00:27:48 2017 +0400;sort commits +romanov73;Wed Nov 8 23:16:07 2017 +0400;extend commits log +romanov73;Wed Nov 8 21:42:16 2017 +0400;fix styles +romanov73;Wed Nov 8 19:17:08 2017 +0400;add logout +romanov73;Wed Nov 8 19:13:17 2017 +0400;add logout +romanov73;Wed Nov 8 20:18:35 2017 +0400;Merge branch 'master' of gitlab.com:romanov73/balance +romanov73;Wed Nov 8 20:17:55 2017 +0400;add database diagramm +Romanov Anton;Wed Nov 8 04:13:39 2017 +0000;Update README.md +Romanov Anton;Wed Nov 8 04:11:33 2017 +0000;Update README.md +romanov73;Wed Nov 8 07:48:13 2017 +0400;Merge remote-tracking branch 'origin/master' +Romanov Anton;Tue Nov 7 18:05:04 2017 +0000;Update README.md +romanov73;Tue Nov 7 22:01:50 2017 +0400;add example of permissions validation +romanov73;Tue Nov 7 20:56:31 2017 +0400;rename project +romanov73;Wed Oct 11 21:37:49 2017 +0400;reverse sort commits +romanov73;Wed Oct 11 21:32:19 2017 +0400;fix read resource as file +romanov73;Wed Oct 11 20:23:10 2017 +0400;Merge remote-tracking branch 'origin/master' +romanov73;Wed Oct 11 20:22:52 2017 +0400;add commits log +Romanov Anton;Tue Oct 10 20:47:29 2017 +0000;Update README.md +romanov73;Fri Oct 6 01:55:57 2017 +0400;add menu to platform \ No newline at end of file diff --git a/src/main/resources/db/changelog-20180301_130000-schema.xml b/src/main/resources/db/changelog-20180301_130000-schema.xml new file mode 100644 index 0000000..47c29e2 --- /dev/null +++ b/src/main/resources/db/changelog-20180301_130000-schema.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog-20180301_140000-data.xml b/src/main/resources/db/changelog-20180301_140000-data.xml new file mode 100644 index 0000000..6d23cba --- /dev/null +++ b/src/main/resources/db/changelog-20180301_140000-data.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog-20180301_140000-schema.xml b/src/main/resources/db/changelog-20180301_140000-schema.xml new file mode 100644 index 0000000..07ab917 --- /dev/null +++ b/src/main/resources/db/changelog-20180301_140000-schema.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog-20180305_100000-schema.xml b/src/main/resources/db/changelog-20180305_100000-schema.xml new file mode 100644 index 0000000..a9e1577 --- /dev/null +++ b/src/main/resources/db/changelog-20180305_100000-schema.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog-20180321_193000-data.xml b/src/main/resources/db/changelog-20180321_193000-data.xml new file mode 100644 index 0000000..d4d9812 --- /dev/null +++ b/src/main/resources/db/changelog-20180321_193000-data.xml @@ -0,0 +1,11 @@ + + + + + update users + set password_hash='$2a$10$5UCtAX/UcNSAcLnHUJDqUO6GR4hyPwCzBfuFI81nsoaYkvNF9SGxG' where id = 1; + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog-20180405_110000-schema.xml b/src/main/resources/db/changelog-20180405_110000-schema.xml new file mode 100644 index 0000000..e174300 --- /dev/null +++ b/src/main/resources/db/changelog-20180405_110000-schema.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog-20180428_110000-schema.xml b/src/main/resources/db/changelog-20180428_110000-schema.xml new file mode 100644 index 0000000..2f1917f --- /dev/null +++ b/src/main/resources/db/changelog-20180428_110000-schema.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog-master.xml b/src/main/resources/db/changelog-master.xml new file mode 100644 index 0000000..713679c --- /dev/null +++ b/src/main/resources/db/changelog-master.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mail_templates/activationEmail.html b/src/main/resources/mail_templates/activationEmail.html new file mode 100644 index 0000000..053c795 --- /dev/null +++ b/src/main/resources/mail_templates/activationEmail.html @@ -0,0 +1,25 @@ + + + + Account activation + + + + +

+ Dear Ivan Ivanov +

+

+ Your account has been created, please click on the URL below to activate it: +

+

+ Activation Link +

+

+ Regards, +
+ Balance Team. +

+ + diff --git a/src/main/resources/mail_templates/passwordResetEmail.html b/src/main/resources/mail_templates/passwordResetEmail.html new file mode 100644 index 0000000..a1034d4 --- /dev/null +++ b/src/main/resources/mail_templates/passwordResetEmail.html @@ -0,0 +1,25 @@ + + + + Password reset + + + + +

+ Dear Ivan Ivanov +

+

+ For your account a password reset was requested, please click on the URL below to +

+

+ Reset Link +

+

+ Regards, +
+ Balance Team. +

+ + diff --git a/css/agency.css b/src/main/resources/public/css/agency.css similarity index 100% rename from css/agency.css rename to src/main/resources/public/css/agency.css diff --git a/css/agency.min.css b/src/main/resources/public/css/agency.min.css similarity index 100% rename from css/agency.min.css rename to src/main/resources/public/css/agency.min.css diff --git a/img/_header-bg.jpg b/src/main/resources/public/img/_header-bg.jpg similarity index 100% rename from img/_header-bg.jpg rename to src/main/resources/public/img/_header-bg.jpg diff --git a/img/about/1.jpg b/src/main/resources/public/img/about/1.jpg similarity index 100% rename from img/about/1.jpg rename to src/main/resources/public/img/about/1.jpg diff --git a/img/about/2.jpg b/src/main/resources/public/img/about/2.jpg similarity index 100% rename from img/about/2.jpg rename to src/main/resources/public/img/about/2.jpg diff --git a/img/about/3.jpg b/src/main/resources/public/img/about/3.jpg similarity index 100% rename from img/about/3.jpg rename to src/main/resources/public/img/about/3.jpg diff --git a/img/about/4.jpg b/src/main/resources/public/img/about/4.jpg similarity index 100% rename from img/about/4.jpg rename to src/main/resources/public/img/about/4.jpg diff --git a/img/header-bg.jpg b/src/main/resources/public/img/header-bg.jpg similarity index 100% rename from img/header-bg.jpg rename to src/main/resources/public/img/header-bg.jpg diff --git a/img/main/career.jpg b/src/main/resources/public/img/main/career.jpg similarity index 100% rename from img/main/career.jpg rename to src/main/resources/public/img/main/career.jpg diff --git a/img/main/conf.jpg b/src/main/resources/public/img/main/conf.jpg similarity index 100% rename from img/main/conf.jpg rename to src/main/resources/public/img/main/conf.jpg diff --git a/img/main/grants.jpg b/src/main/resources/public/img/main/grants.jpg similarity index 100% rename from img/main/grants.jpg rename to src/main/resources/public/img/main/grants.jpg diff --git a/img/main/papers.jpg b/src/main/resources/public/img/main/papers.jpg similarity index 100% rename from img/main/papers.jpg rename to src/main/resources/public/img/main/papers.jpg diff --git a/img/main/projects.jpg b/src/main/resources/public/img/main/projects.jpg similarity index 100% rename from img/main/projects.jpg rename to src/main/resources/public/img/main/projects.jpg diff --git a/img/main/students.jpg b/src/main/resources/public/img/main/students.jpg similarity index 100% rename from img/main/students.jpg rename to src/main/resources/public/img/main/students.jpg diff --git a/img/main/tasks.jpg b/src/main/resources/public/img/main/tasks.jpg similarity index 100% rename from img/main/tasks.jpg rename to src/main/resources/public/img/main/tasks.jpg diff --git a/img/main/team.jpg b/src/main/resources/public/img/main/team.jpg similarity index 100% rename from img/main/team.jpg rename to src/main/resources/public/img/main/team.jpg diff --git a/img/main/templates.jpg b/src/main/resources/public/img/main/templates.jpg similarity index 100% rename from img/main/templates.jpg rename to src/main/resources/public/img/main/templates.jpg diff --git a/img/map-image.png b/src/main/resources/public/img/map-image.png similarity index 100% rename from img/map-image.png rename to src/main/resources/public/img/map-image.png diff --git a/js/agency.js b/src/main/resources/public/js/agency.js similarity index 100% rename from js/agency.js rename to src/main/resources/public/js/agency.js diff --git a/js/agency.min.js b/src/main/resources/public/js/agency.min.js similarity index 100% rename from js/agency.min.js rename to src/main/resources/public/js/agency.min.js diff --git a/js/contact_me.js b/src/main/resources/public/js/contact_me.js similarity index 100% rename from js/contact_me.js rename to src/main/resources/public/js/contact_me.js diff --git a/js/contact_me.min.js b/src/main/resources/public/js/contact_me.min.js similarity index 100% rename from js/contact_me.min.js rename to src/main/resources/public/js/contact_me.min.js diff --git a/js/jqBootstrapValidation.js b/src/main/resources/public/js/jqBootstrapValidation.js similarity index 100% rename from js/jqBootstrapValidation.js rename to src/main/resources/public/js/jqBootstrapValidation.js diff --git a/js/jqBootstrapValidation.min.js b/src/main/resources/public/js/jqBootstrapValidation.min.js similarity index 100% rename from js/jqBootstrapValidation.min.js rename to src/main/resources/public/js/jqBootstrapValidation.min.js diff --git a/mail/contact_me.php b/src/main/resources/public/mail/contact_me.php similarity index 100% rename from mail/contact_me.php rename to src/main/resources/public/mail/contact_me.php diff --git a/scss/_contact.scss b/src/main/resources/public/scss/_contact.scss similarity index 100% rename from scss/_contact.scss rename to src/main/resources/public/scss/_contact.scss diff --git a/scss/_footer.scss b/src/main/resources/public/scss/_footer.scss similarity index 100% rename from scss/_footer.scss rename to src/main/resources/public/scss/_footer.scss diff --git a/scss/_global.scss b/src/main/resources/public/scss/_global.scss similarity index 100% rename from scss/_global.scss rename to src/main/resources/public/scss/_global.scss diff --git a/scss/_masthead.scss b/src/main/resources/public/scss/_masthead.scss similarity index 100% rename from scss/_masthead.scss rename to src/main/resources/public/scss/_masthead.scss diff --git a/scss/_mixins.scss b/src/main/resources/public/scss/_mixins.scss similarity index 100% rename from scss/_mixins.scss rename to src/main/resources/public/scss/_mixins.scss diff --git a/scss/_navbar.scss b/src/main/resources/public/scss/_navbar.scss similarity index 100% rename from scss/_navbar.scss rename to src/main/resources/public/scss/_navbar.scss diff --git a/scss/_portfolio.scss b/src/main/resources/public/scss/_portfolio.scss similarity index 100% rename from scss/_portfolio.scss rename to src/main/resources/public/scss/_portfolio.scss diff --git a/scss/_services.scss b/src/main/resources/public/scss/_services.scss similarity index 100% rename from scss/_services.scss rename to src/main/resources/public/scss/_services.scss diff --git a/scss/_team.scss b/src/main/resources/public/scss/_team.scss similarity index 100% rename from scss/_team.scss rename to src/main/resources/public/scss/_team.scss diff --git a/scss/_timeline.scss b/src/main/resources/public/scss/_timeline.scss similarity index 100% rename from scss/_timeline.scss rename to src/main/resources/public/scss/_timeline.scss diff --git a/scss/_variables.scss b/src/main/resources/public/scss/_variables.scss similarity index 100% rename from scss/_variables.scss rename to src/main/resources/public/scss/_variables.scss diff --git a/scss/agency.scss b/src/main/resources/public/scss/agency.scss similarity index 100% rename from scss/agency.scss rename to src/main/resources/public/scss/agency.scss diff --git a/vendor/bootstrap/css/bootstrap.css b/src/main/resources/public/vendor/bootstrap/css/bootstrap.css similarity index 100% rename from vendor/bootstrap/css/bootstrap.css rename to src/main/resources/public/vendor/bootstrap/css/bootstrap.css diff --git a/vendor/bootstrap/css/bootstrap.css.map b/src/main/resources/public/vendor/bootstrap/css/bootstrap.css.map similarity index 100% rename from vendor/bootstrap/css/bootstrap.css.map rename to src/main/resources/public/vendor/bootstrap/css/bootstrap.css.map diff --git a/vendor/bootstrap/css/bootstrap.min.css b/src/main/resources/public/vendor/bootstrap/css/bootstrap.min.css similarity index 100% rename from vendor/bootstrap/css/bootstrap.min.css rename to src/main/resources/public/vendor/bootstrap/css/bootstrap.min.css diff --git a/vendor/bootstrap/css/bootstrap.min.css.map b/src/main/resources/public/vendor/bootstrap/css/bootstrap.min.css.map similarity index 100% rename from vendor/bootstrap/css/bootstrap.min.css.map rename to src/main/resources/public/vendor/bootstrap/css/bootstrap.min.css.map diff --git a/vendor/bootstrap/js/bootstrap.bundle.js b/src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.js similarity index 100% rename from vendor/bootstrap/js/bootstrap.bundle.js rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.js diff --git a/vendor/bootstrap/js/bootstrap.bundle.js.map b/src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.js.map similarity index 100% rename from vendor/bootstrap/js/bootstrap.bundle.js.map rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.js.map diff --git a/vendor/bootstrap/js/bootstrap.bundle.min.js b/src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.min.js similarity index 100% rename from vendor/bootstrap/js/bootstrap.bundle.min.js rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.min.js diff --git a/vendor/bootstrap/js/bootstrap.bundle.min.js.map b/src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.min.js.map similarity index 100% rename from vendor/bootstrap/js/bootstrap.bundle.min.js.map rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.bundle.min.js.map diff --git a/vendor/bootstrap/js/bootstrap.js b/src/main/resources/public/vendor/bootstrap/js/bootstrap.js similarity index 100% rename from vendor/bootstrap/js/bootstrap.js rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.js diff --git a/vendor/bootstrap/js/bootstrap.js.map b/src/main/resources/public/vendor/bootstrap/js/bootstrap.js.map similarity index 100% rename from vendor/bootstrap/js/bootstrap.js.map rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.js.map diff --git a/vendor/bootstrap/js/bootstrap.min.js b/src/main/resources/public/vendor/bootstrap/js/bootstrap.min.js similarity index 100% rename from vendor/bootstrap/js/bootstrap.min.js rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.min.js diff --git a/vendor/bootstrap/js/bootstrap.min.js.map b/src/main/resources/public/vendor/bootstrap/js/bootstrap.min.js.map similarity index 100% rename from vendor/bootstrap/js/bootstrap.min.js.map rename to src/main/resources/public/vendor/bootstrap/js/bootstrap.min.js.map diff --git a/vendor/font-awesome/css/font-awesome.css b/src/main/resources/public/vendor/font-awesome/css/font-awesome.css similarity index 100% rename from vendor/font-awesome/css/font-awesome.css rename to src/main/resources/public/vendor/font-awesome/css/font-awesome.css diff --git a/vendor/font-awesome/css/font-awesome.css.map b/src/main/resources/public/vendor/font-awesome/css/font-awesome.css.map similarity index 100% rename from vendor/font-awesome/css/font-awesome.css.map rename to src/main/resources/public/vendor/font-awesome/css/font-awesome.css.map diff --git a/vendor/font-awesome/css/font-awesome.min.css b/src/main/resources/public/vendor/font-awesome/css/font-awesome.min.css similarity index 100% rename from vendor/font-awesome/css/font-awesome.min.css rename to src/main/resources/public/vendor/font-awesome/css/font-awesome.min.css diff --git a/vendor/font-awesome/fonts/FontAwesome.otf b/src/main/resources/public/vendor/font-awesome/fonts/FontAwesome.otf similarity index 100% rename from vendor/font-awesome/fonts/FontAwesome.otf rename to src/main/resources/public/vendor/font-awesome/fonts/FontAwesome.otf diff --git a/vendor/font-awesome/fonts/fontawesome-webfont.eot b/src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.eot similarity index 100% rename from vendor/font-awesome/fonts/fontawesome-webfont.eot rename to src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.eot diff --git a/vendor/font-awesome/fonts/fontawesome-webfont.svg b/src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.svg similarity index 100% rename from vendor/font-awesome/fonts/fontawesome-webfont.svg rename to src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.svg diff --git a/vendor/font-awesome/fonts/fontawesome-webfont.ttf b/src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.ttf similarity index 100% rename from vendor/font-awesome/fonts/fontawesome-webfont.ttf rename to src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.ttf diff --git a/vendor/font-awesome/fonts/fontawesome-webfont.woff b/src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.woff similarity index 100% rename from vendor/font-awesome/fonts/fontawesome-webfont.woff rename to src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.woff diff --git a/vendor/font-awesome/fonts/fontawesome-webfont.woff2 b/src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.woff2 similarity index 100% rename from vendor/font-awesome/fonts/fontawesome-webfont.woff2 rename to src/main/resources/public/vendor/font-awesome/fonts/fontawesome-webfont.woff2 diff --git a/vendor/font-awesome/less/animated.less b/src/main/resources/public/vendor/font-awesome/less/animated.less similarity index 100% rename from vendor/font-awesome/less/animated.less rename to src/main/resources/public/vendor/font-awesome/less/animated.less diff --git a/vendor/font-awesome/less/bordered-pulled.less b/src/main/resources/public/vendor/font-awesome/less/bordered-pulled.less similarity index 100% rename from vendor/font-awesome/less/bordered-pulled.less rename to src/main/resources/public/vendor/font-awesome/less/bordered-pulled.less diff --git a/vendor/font-awesome/less/core.less b/src/main/resources/public/vendor/font-awesome/less/core.less similarity index 100% rename from vendor/font-awesome/less/core.less rename to src/main/resources/public/vendor/font-awesome/less/core.less diff --git a/vendor/font-awesome/less/fixed-width.less b/src/main/resources/public/vendor/font-awesome/less/fixed-width.less similarity index 100% rename from vendor/font-awesome/less/fixed-width.less rename to src/main/resources/public/vendor/font-awesome/less/fixed-width.less diff --git a/vendor/font-awesome/less/font-awesome.less b/src/main/resources/public/vendor/font-awesome/less/font-awesome.less similarity index 100% rename from vendor/font-awesome/less/font-awesome.less rename to src/main/resources/public/vendor/font-awesome/less/font-awesome.less diff --git a/vendor/font-awesome/less/icons.less b/src/main/resources/public/vendor/font-awesome/less/icons.less similarity index 100% rename from vendor/font-awesome/less/icons.less rename to src/main/resources/public/vendor/font-awesome/less/icons.less diff --git a/vendor/font-awesome/less/larger.less b/src/main/resources/public/vendor/font-awesome/less/larger.less similarity index 100% rename from vendor/font-awesome/less/larger.less rename to src/main/resources/public/vendor/font-awesome/less/larger.less diff --git a/vendor/font-awesome/less/list.less b/src/main/resources/public/vendor/font-awesome/less/list.less similarity index 100% rename from vendor/font-awesome/less/list.less rename to src/main/resources/public/vendor/font-awesome/less/list.less diff --git a/vendor/font-awesome/less/mixins.less b/src/main/resources/public/vendor/font-awesome/less/mixins.less similarity index 100% rename from vendor/font-awesome/less/mixins.less rename to src/main/resources/public/vendor/font-awesome/less/mixins.less diff --git a/vendor/font-awesome/less/path.less b/src/main/resources/public/vendor/font-awesome/less/path.less similarity index 100% rename from vendor/font-awesome/less/path.less rename to src/main/resources/public/vendor/font-awesome/less/path.less diff --git a/vendor/font-awesome/less/rotated-flipped.less b/src/main/resources/public/vendor/font-awesome/less/rotated-flipped.less similarity index 100% rename from vendor/font-awesome/less/rotated-flipped.less rename to src/main/resources/public/vendor/font-awesome/less/rotated-flipped.less diff --git a/vendor/font-awesome/less/screen-reader.less b/src/main/resources/public/vendor/font-awesome/less/screen-reader.less similarity index 100% rename from vendor/font-awesome/less/screen-reader.less rename to src/main/resources/public/vendor/font-awesome/less/screen-reader.less diff --git a/vendor/font-awesome/less/stacked.less b/src/main/resources/public/vendor/font-awesome/less/stacked.less similarity index 100% rename from vendor/font-awesome/less/stacked.less rename to src/main/resources/public/vendor/font-awesome/less/stacked.less diff --git a/vendor/font-awesome/less/variables.less b/src/main/resources/public/vendor/font-awesome/less/variables.less similarity index 100% rename from vendor/font-awesome/less/variables.less rename to src/main/resources/public/vendor/font-awesome/less/variables.less diff --git a/vendor/font-awesome/scss/_animated.scss b/src/main/resources/public/vendor/font-awesome/scss/_animated.scss similarity index 100% rename from vendor/font-awesome/scss/_animated.scss rename to src/main/resources/public/vendor/font-awesome/scss/_animated.scss diff --git a/vendor/font-awesome/scss/_bordered-pulled.scss b/src/main/resources/public/vendor/font-awesome/scss/_bordered-pulled.scss similarity index 100% rename from vendor/font-awesome/scss/_bordered-pulled.scss rename to src/main/resources/public/vendor/font-awesome/scss/_bordered-pulled.scss diff --git a/vendor/font-awesome/scss/_core.scss b/src/main/resources/public/vendor/font-awesome/scss/_core.scss similarity index 100% rename from vendor/font-awesome/scss/_core.scss rename to src/main/resources/public/vendor/font-awesome/scss/_core.scss diff --git a/vendor/font-awesome/scss/_fixed-width.scss b/src/main/resources/public/vendor/font-awesome/scss/_fixed-width.scss similarity index 100% rename from vendor/font-awesome/scss/_fixed-width.scss rename to src/main/resources/public/vendor/font-awesome/scss/_fixed-width.scss diff --git a/vendor/font-awesome/scss/_icons.scss b/src/main/resources/public/vendor/font-awesome/scss/_icons.scss similarity index 100% rename from vendor/font-awesome/scss/_icons.scss rename to src/main/resources/public/vendor/font-awesome/scss/_icons.scss diff --git a/vendor/font-awesome/scss/_larger.scss b/src/main/resources/public/vendor/font-awesome/scss/_larger.scss similarity index 100% rename from vendor/font-awesome/scss/_larger.scss rename to src/main/resources/public/vendor/font-awesome/scss/_larger.scss diff --git a/vendor/font-awesome/scss/_list.scss b/src/main/resources/public/vendor/font-awesome/scss/_list.scss similarity index 100% rename from vendor/font-awesome/scss/_list.scss rename to src/main/resources/public/vendor/font-awesome/scss/_list.scss diff --git a/vendor/font-awesome/scss/_mixins.scss b/src/main/resources/public/vendor/font-awesome/scss/_mixins.scss similarity index 100% rename from vendor/font-awesome/scss/_mixins.scss rename to src/main/resources/public/vendor/font-awesome/scss/_mixins.scss diff --git a/vendor/font-awesome/scss/_path.scss b/src/main/resources/public/vendor/font-awesome/scss/_path.scss similarity index 100% rename from vendor/font-awesome/scss/_path.scss rename to src/main/resources/public/vendor/font-awesome/scss/_path.scss diff --git a/vendor/font-awesome/scss/_rotated-flipped.scss b/src/main/resources/public/vendor/font-awesome/scss/_rotated-flipped.scss similarity index 100% rename from vendor/font-awesome/scss/_rotated-flipped.scss rename to src/main/resources/public/vendor/font-awesome/scss/_rotated-flipped.scss diff --git a/vendor/font-awesome/scss/_screen-reader.scss b/src/main/resources/public/vendor/font-awesome/scss/_screen-reader.scss similarity index 100% rename from vendor/font-awesome/scss/_screen-reader.scss rename to src/main/resources/public/vendor/font-awesome/scss/_screen-reader.scss diff --git a/vendor/font-awesome/scss/_stacked.scss b/src/main/resources/public/vendor/font-awesome/scss/_stacked.scss similarity index 100% rename from vendor/font-awesome/scss/_stacked.scss rename to src/main/resources/public/vendor/font-awesome/scss/_stacked.scss diff --git a/vendor/font-awesome/scss/_variables.scss b/src/main/resources/public/vendor/font-awesome/scss/_variables.scss similarity index 100% rename from vendor/font-awesome/scss/_variables.scss rename to src/main/resources/public/vendor/font-awesome/scss/_variables.scss diff --git a/vendor/font-awesome/scss/font-awesome.scss b/src/main/resources/public/vendor/font-awesome/scss/font-awesome.scss similarity index 100% rename from vendor/font-awesome/scss/font-awesome.scss rename to src/main/resources/public/vendor/font-awesome/scss/font-awesome.scss diff --git a/vendor/google/droid.css b/src/main/resources/public/vendor/google/droid.css similarity index 100% rename from vendor/google/droid.css rename to src/main/resources/public/vendor/google/droid.css diff --git a/vendor/google/kaushan.css b/src/main/resources/public/vendor/google/kaushan.css similarity index 100% rename from vendor/google/kaushan.css rename to src/main/resources/public/vendor/google/kaushan.css diff --git a/vendor/google/montserrat.css b/src/main/resources/public/vendor/google/montserrat.css similarity index 100% rename from vendor/google/montserrat.css rename to src/main/resources/public/vendor/google/montserrat.css diff --git a/vendor/google/roboto.css b/src/main/resources/public/vendor/google/roboto.css similarity index 100% rename from vendor/google/roboto.css rename to src/main/resources/public/vendor/google/roboto.css diff --git a/vendor/jquery-easing/jquery.easing.compatibility.js b/src/main/resources/public/vendor/jquery-easing/jquery.easing.compatibility.js similarity index 100% rename from vendor/jquery-easing/jquery.easing.compatibility.js rename to src/main/resources/public/vendor/jquery-easing/jquery.easing.compatibility.js diff --git a/vendor/jquery-easing/jquery.easing.js b/src/main/resources/public/vendor/jquery-easing/jquery.easing.js similarity index 100% rename from vendor/jquery-easing/jquery.easing.js rename to src/main/resources/public/vendor/jquery-easing/jquery.easing.js diff --git a/vendor/jquery-easing/jquery.easing.min.js b/src/main/resources/public/vendor/jquery-easing/jquery.easing.min.js similarity index 100% rename from vendor/jquery-easing/jquery.easing.min.js rename to src/main/resources/public/vendor/jquery-easing/jquery.easing.min.js diff --git a/vendor/jquery/jquery.js b/src/main/resources/public/vendor/jquery/jquery.js similarity index 100% rename from vendor/jquery/jquery.js rename to src/main/resources/public/vendor/jquery/jquery.js diff --git a/vendor/jquery/jquery.min.js b/src/main/resources/public/vendor/jquery/jquery.min.js similarity index 100% rename from vendor/jquery/jquery.min.js rename to src/main/resources/public/vendor/jquery/jquery.min.js diff --git a/vendor/jquery/jquery.min.map b/src/main/resources/public/vendor/jquery/jquery.min.map similarity index 100% rename from vendor/jquery/jquery.min.map rename to src/main/resources/public/vendor/jquery/jquery.min.map diff --git a/vendor/jquery/jquery.slim.js b/src/main/resources/public/vendor/jquery/jquery.slim.js similarity index 100% rename from vendor/jquery/jquery.slim.js rename to src/main/resources/public/vendor/jquery/jquery.slim.js diff --git a/vendor/jquery/jquery.slim.min.js b/src/main/resources/public/vendor/jquery/jquery.slim.min.js similarity index 100% rename from vendor/jquery/jquery.slim.min.js rename to src/main/resources/public/vendor/jquery/jquery.slim.min.js diff --git a/vendor/jquery/jquery.slim.min.map b/src/main/resources/public/vendor/jquery/jquery.slim.min.map similarity index 100% rename from vendor/jquery/jquery.slim.min.map rename to src/main/resources/public/vendor/jquery/jquery.slim.min.map diff --git a/src/main/resources/sample.jks b/src/main/resources/sample.jks new file mode 100644 index 0000000000000000000000000000000000000000..6aa9a28053a591e41453e665e5024e8a8cb78b3d GIT binary patch literal 2264 zcmchYX*3iJ7sqE|hQS!q5Mv)4GM2$i#uAFqC`%7x7baWA*i&dRX>3`uq(XS?3XSYp z%38`&ib7E$8j~$cF^}gt?|I+noW8#w?uYxk=iGD8|K9Vzd#pVc0002(2k@T|2@MMI zqxqr2AhQO*TVi`j@((S;e;g;l$#dAA{>vf0kX$R(Qn4oKgGEYjZ5zti2dw?Z6A zh%LuFCNI?9o+Z1duJL-++e#cjO`zlK?u9s030=k_*wD1#-$FbIDRDnA^vo@fm( zzjt(3VJrGOr0iHXSTM|rYN#>RZ@Dp`PwB2zrDQffLvuoR2~V3ReYa0&vU^dXd8isV zsAf*@!8s%xBvHLseXn6f?1kefe(8uAmAbaF$x{Ykzb6c6jdUwY1$y4tFzsj7 zIghr!T#ODfu@Po!a29@kXQ8kY#(LE<0o7?7PQ|eMeY@Equ?R-6*f@Na3o&stDQ=6( zQzDSQhCnS(9Bu9W_~giknP0vECqUsr4_9y_}nEU`cy z4}dApnAip92wMwgzciAFpc3i}+-#Zlq+iF7d1y}d4Qsp8=%l1N8NIs161I`HmkcpQ zY4*CUCFJJf(2!M{`&qQ}3($KeTQ=)mMrBs`DOb;%Of0tC)9he_p~w&CO#DfCgx(%s z{@|D(brX_Gb}ZDLmGej*JgEl0Et>q~kgTXuJg-PwvRjNx8sBbIShxD=xOySzw{;^X zAvrh5HTg>Xq@<{#^!Kg}B?qz@b<{ebD)yaSf&RChBIJQo-?Ahzw@qopSe^e&>^IuU zydM4Y1_C&>k7u|}=; z63R7$H6zat=hNExxEwXu1fQ*ytuEkP!{w{|#6TIEq1#*ck=6_NM*ILF65tmD-O5&R zMI!-MT<3U~t@}(CN4@RlZ~1I>C=!ywF)dNI{VvH;5Y3(Z4jY^%_c&fsm4Q`<1g|qX z&!h29jXjVE3nJnet*L)XL?-8<>qDbVGP%i^NwOZfwWO7?Mr!X7 zl}sG@9S_5}}td}$xrWIYY=e(VVBiv%A+M-{M z!3_^Tc=pV?niT!{D`!{e@W;MvrZ(OER{x7itVAtwE~spPtPtma|J=5dv&_oE!5H#` zdgXJ;+gJ4hI}*9QX9jpL`Gb)yCe%1}t!&O-^sihyZys%%5uF~WhsR_w(q7;vV5d4P zr%ZUA2}kO+L^2ePTgGT9Ua71w<+)poSyjTdLq&xbUn`<6&SpwFp(HRHUyU6J3WZ_! zfztko79+94Tq%mTYj53(RYcL&1~5`I#+w3`(Q|r+P(aT z%?r(^?IWw~19CB&uvXf(f7&BnEE{zwK4piVU`I4j1j?v5d4N<7VUJ8nM`$7S*mfKR z#9-JzPRZ?{M!@L+0N^V)IyeeP2T|^UK|m0QD+Ibs!wEoml^N!YO#vW~j~jraX(0A3 z6Kux?IRLez`O^X;{!4g%BhcRn>^H*qKZ3*|{_YGuz)KCJcu;)DSES5D2tDE`C02YR0R%Vy1T7k|RQ;3g<0icA$AuP0pOvc~jGl zz+NeKv_FT_;GWK&8XlDUv&hv9kxg?@c!bu?83i=YQ$S!K09Y)Glg3Hz?@|)ZCBlVz zP8i}#XZkMoje3I=h&I!!s_m?Qi@1MR`yv7X*yEs47qOs^t^?&=;*IQ!q&)gq_Sx5* z?fhU8Q*PSe*w7y)FH#P!9R^Xw!lTT+zI39L<&8cViaj$A(Z2Cg7!{V?uuyi#vlNCg z40i}2ivw&y&1-&Nh&WMG`&aIt>)(#tKTJ}^@696Kw1-{IzSOTnFF+0@k$o3%ZHS;Q#;t literal 0 HcmV?d00001 diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d73fd0d57af85ba42b0990703fcb44093c665fbe GIT binary patch literal 2166 zcmeH|OK@6M7{~u0R%*1qAN5g9HBIk@3tZq{HKMVhu@zsSMr*2tYJ!TW(TERfTYRKa zQx&VkM~o#JqZPGq78z!;fEAle7R+RqU3S@J$ByxTXyQ1XapBq?=6AmT{T}Cj=Nkq9 z3qRYofx8_sJ8&(4orEp~i1h#4Qo1^!5(jY-rK_8`psMr{C(($~Wsm?=x70ysw3;e? zArgV22H`YhY#%j!?f`X^Is&by6E5OV8FS0rI9$EVRg=(q`960N?tY%vFZC$3v}w3P zE_lM-gm!?wo-FI=4+o)!qRSwCN;7St_xYf@L?Dlxxafl1tR zP12%f%|aFa+A7@hEqH2c@Yk35yawNbj4iUh2_Ny)TF{Bvs6$_1-^6Ti7Io=chOxkV z5u>p}+hh*kO?<>`$XuS;U~j}EdV?|ICk8Re`SpLCz$W=~hucPe47G2+ssDqYzj^rL zzb9dQb)VEv3$agHOH54Op^d$IjeL-6u)LeQ^$j87HVW@=<-NdFM_=lYL9RU_!hLg2 zv+yv;*(3I``~vsOT)?&M_A|oW7aw00WSDZ z9AR&|EAadMXX7|fU}m2T z+4p|>^`6b`_?8?mHYDD$^?iDU6z%@j&u*LjhdV5qGk5+oNUcuo;IVO= zef$Yv`?PrGoVYc$*M36qmO~$Pz>*LruRbSX*V~un#Pc@v{lXV`PnX-HxhQ=7M%tFF z`Ms>)KK+~xmLW0O$;cjAvA!OA@symt^-$qY;U?TG$FaWLnMbaa^3li7eldDSzj*g`-$wMj I{qGa_3*J3ebN~PV literal 0 HcmV?d00001 diff --git a/src/main/resources/static/favicon_big.ico b/src/main/resources/static/favicon_big.ico new file mode 100644 index 0000000000000000000000000000000000000000..94e3bc0676dfc98e22ed90241ff2bc5d9d54d2ce GIT binary patch literal 4614 zcmeH}O>kOO6vzJ{*3@YI{;1!nrYR2|@PLOJF|nbs6~91@)>I3v2`ZvSBYsd@Q5&VE zDprXf5lb{iD{A2`GR$NFD>j)dn8_}??6PCWj`4p}x6ZtbTQ2i^=e%>z|J?h|yTd%d z#NU7hUc1n|3b-D?9x_0V5*h!mVeRdKLTtoNti6520Yzbq*ojK4y*lwjaY{b`l~z?` zEJ(tzDgoGaId_mgV@^N)2>meB{vJ4pP2rqV=EmR{WUi8cI>_~$2{?yYZ&>=1^rcO~ z5p=*6>LauxjP+-zV>}dq5{y6_m9ZJvLt*+3Scg;4qRb!lvsQ*V8OHL^#<|bnJRDJK z;sIDg^Kei2;f!-XBSpBA0eCjyzDFz2k}+7L6?oFTk5-|KS705NwhGTw7`C{8eWD0$ zDh4&3f;C=;XF38~vI1pd1>S6g#Gs{%P?D?g=3>xu3Fxy4DCt#b**x^ZBy3ZhE2QYB z=&zA==nM3x*I~b)iMt(3kkECDyJL;ay}6TD3_1QkA}---f#;{VE(K=GQCm z5$9rywCPi`T!pu`0q0U1u38h``Wo+>@GQ%@73y2?5Lc}YjrbaMXv^F;@io|sx{R$s zU*>ZWy|GT)Vh-0P9^z}rT-I!GZ^TEm2Iq*E=)^}lqtk&-2Ra?-bm0GVV4M7Wz?~C6 z2REy18~*6m?;gF>a7F!_2W0NELL8LV6cw{~t5dICCvS|5(egf~ncoy5W})N$qkI6k z=7X2Jjfk1EUxazxo)x*{QgTQXZl|%%Kg_F zy~TduY03_>f#zf4iz{*2eb2xD%kTGZH>BT@<%=5=?^=dF=eOk8h5i{*{+o2@c(>_Q zk+AIf$tGXN`Fpk^X0DRo&27glUDHniyXM4mThyDA2f9uRzQwVV-7v+)nQOOLVDCFu zWW@^>4E@FjaGxu09-T`tf)xv!#91+>t83cnb#P qaWFWX>Bp{?sgFN-;g4f*@~ijW@E8<#(mEaJbfD9LP6xKofqwugqgRFi literal 0 HcmV?d00001 diff --git a/src/main/resources/templates/activate.html b/src/main/resources/templates/activate.html new file mode 100644 index 0000000..b20a284 --- /dev/null +++ b/src/main/resources/templates/activate.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/commits.html b/src/main/resources/templates/admin/commits.html new file mode 100644 index 0000000..a8b8e85 --- /dev/null +++ b/src/main/resources/templates/admin/commits.html @@ -0,0 +1,24 @@ + + + + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/userList.html b/src/main/resources/templates/admin/userList.html new file mode 100644 index 0000000..b47f720 --- /dev/null +++ b/src/main/resources/templates/admin/userList.html @@ -0,0 +1,24 @@ + + + + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/userSessions.html b/src/main/resources/templates/admin/userSessions.html new file mode 100644 index 0000000..2c3405a --- /dev/null +++ b/src/main/resources/templates/admin/userSessions.html @@ -0,0 +1,24 @@ + + + + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/default.html b/src/main/resources/templates/default.html new file mode 100644 index 0000000..826623b --- /dev/null +++ b/src/main/resources/templates/default.html @@ -0,0 +1,61 @@ + + + + + + + + + + NG-Tacker + + + + + + + + + + + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html new file mode 100644 index 0000000..2a64877 --- /dev/null +++ b/src/main/resources/templates/error/403.html @@ -0,0 +1,13 @@ + + + + + +
+

Доступ запрещён

+

Вернуться на главную

+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html new file mode 100644 index 0000000..9cb1cd2 --- /dev/null +++ b/src/main/resources/templates/error/404.html @@ -0,0 +1,13 @@ + + + + + +
+

Страница не найдена

+

Вернуться на главную

+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/error/500.html b/src/main/resources/templates/error/500.html new file mode 100644 index 0000000..75f84a1 --- /dev/null +++ b/src/main/resources/templates/error/500.html @@ -0,0 +1,13 @@ + + + + + +
+

Ошибка сервера

+

Вернуться на главную

+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..1208764 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,14 @@ + + + + + +
+
+ Домашняя страница +
+
+ + \ No newline at end of file diff --git a/index.html b/src/main/resources/templates/index.html similarity index 72% rename from index.html rename to src/main/resources/templates/index.html index 88d22b4..b362a4c 100644 --- a/index.html +++ b/src/main/resources/templates/index.html @@ -1,59 +1,12 @@ - - + - - - - - - - NG-Tacker - - - - - - - - - - - - - - + - - - - - +
@@ -65,13 +18,13 @@
- +
- +

Статьи

@@ -85,7 +38,7 @@
- +

Гранты

@@ -99,7 +52,7 @@
- +

Проекты

@@ -113,7 +66,7 @@
- +

Конференции

@@ -127,7 +80,7 @@
- +

Команда

@@ -141,7 +94,7 @@
- +

Работа со студентами

@@ -155,7 +108,7 @@
- +

Задачи

@@ -169,7 +122,7 @@
- +

Карьера

@@ -183,7 +136,7 @@
- +

Прочее

@@ -207,7 +160,7 @@ - +
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..fbf9a44 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,117 @@ + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/paper.html b/src/main/resources/templates/paper.html similarity index 100% rename from paper.html rename to src/main/resources/templates/paper.html diff --git a/src/main/resources/templates/papers.html b/src/main/resources/templates/papers.html new file mode 100644 index 0000000..107c479 --- /dev/null +++ b/src/main/resources/templates/papers.html @@ -0,0 +1,63 @@ + + + + + + + + + + diff --git a/src/main/resources/templates/reset.html b/src/main/resources/templates/reset.html new file mode 100644 index 0000000..232e260 --- /dev/null +++ b/src/main/resources/templates/reset.html @@ -0,0 +1,77 @@ + + + + + + +
+
+
+
+ +
+
+ +
+ + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/resetRequest.html b/src/main/resources/templates/resetRequest.html new file mode 100644 index 0000000..2adab37 --- /dev/null +++ b/src/main/resources/templates/resetRequest.html @@ -0,0 +1,57 @@ + + + + + + +
+
+
+
+ +
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/src/test/java/BalanceEmployeeTest.java b/src/test/java/BalanceEmployeeTest.java new file mode 100644 index 0000000..715f585 --- /dev/null +++ b/src/test/java/BalanceEmployeeTest.java @@ -0,0 +1,88 @@ +//import org.junit.jupiter.api.Test; +//import ru.ulstu.platform.balance.boundary.BalanceEmployeeService; +//import ru.ulstu.platform.balance.entity.dto.BalanceEmployeeDto; +//import ru.ulstu.platform.balance.entity.dto.BalanceValueDto; +//import ru.ulstu.platform.tools.entity.WorkType; +//import ru.ulstu.platform.unit.entity.Unit; +// +//import java.util.Arrays; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; + +//import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class BalanceEmployeeTest { + private final static Integer YEAR_2010 = 2010; + private final static Integer YEAR_2011 = 2011; + +// @Test +// public void testGetFreeEmployeesNextUnits() { +// WorkType workType = new WorkType(); +// workType.setId(1L); +// +// BalanceEmployeeService balanceEmployeeService = new BalanceEmployeeService(); +// +// Map> allUnitBalances = new HashMap<>(); +// Unit parent = new Unit(0L); +// Unit currentUnit = new Unit(1L); +// currentUnit.setParent(parent); +// Unit nextUnit = new Unit(2L); +// nextUnit.setParent(parent); +// Unit anotherUnit = new Unit(3L); +// anotherUnit.setParent(parent); +// +// parent.getChildren().addAll(Arrays.asList(currentUnit, nextUnit, anotherUnit)); +// +// Double deficit = 10.0; +// Double proficit = deficit; +// Double fullUseProfict = 0.0; +// +// //move all employees (deficit == proficit) +// BalanceValueDto balanceEmployeeValueDto = new BalanceValueDto(YEAR_2010, 1, workType, deficit * -1); +// allUnitBalances.put(currentUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, deficit * -1))); +// allUnitBalances.put(nextUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, proficit))); +// Double result = balanceEmployeeService.getFreeEmployeesNextUnits(allUnitBalances, currentUnit, balanceEmployeeValueDto); +// assertEquals(result, Double.valueOf(deficit)); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(nextUnit).get(0).getMaxValue(YEAR_2010))); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(currentUnit).get(0).getMaxValue(YEAR_2010))); +// +// //move part employees (deficit < proficit) +// Double proficitDeficitDiff = 5.0; +// proficit = deficit + proficitDeficitDiff; +// allUnitBalances.put(currentUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, deficit * -1))); +// allUnitBalances.put(nextUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, proficit))); +// result = balanceEmployeeService.getFreeEmployeesNextUnits(allUnitBalances, currentUnit, balanceEmployeeValueDto); +// assertEquals(result, Double.valueOf(deficit)); +// assertEquals(Double.valueOf(proficitDeficitDiff), Double.valueOf(allUnitBalances.get(nextUnit).get(0).getMaxValue(YEAR_2010))); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(currentUnit).get(0).getMaxValue(YEAR_2010))); +// +// //move all employees and deficit not compelte (deficit > proficit) +// proficit = deficit - proficitDeficitDiff; +// allUnitBalances.put(currentUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, deficit * -1))); +// allUnitBalances.put(nextUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, proficit))); +// result = balanceEmployeeService.getFreeEmployeesNextUnits(allUnitBalances, currentUnit, balanceEmployeeValueDto); +// assertEquals(result, Double.valueOf(deficit - proficitDeficitDiff)); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(nextUnit).get(0).getMaxValue(YEAR_2010))); +// assertEquals(Double.valueOf(proficitDeficitDiff * -1.0), Double.valueOf(allUnitBalances.get(currentUnit).get(0).getMaxValue(YEAR_2010))); +// +// //collect from several units +// proficit = deficit - proficitDeficitDiff; +// allUnitBalances.put(currentUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, deficit * -1))); +// allUnitBalances.put(nextUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, proficit))); +// allUnitBalances.put(anotherUnit, Arrays.asList(getBalanceDto(balanceEmployeeValueDto, proficit))); +// result = balanceEmployeeService.getFreeEmployeesNextUnits(allUnitBalances, currentUnit, balanceEmployeeValueDto); +// assertEquals(result, Double.valueOf(deficit)); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(nextUnit).get(0).getMaxValue(YEAR_2010))); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(anotherUnit).get(0).getMaxValue(YEAR_2010))); +// assertEquals(Double.valueOf(fullUseProfict), Double.valueOf(allUnitBalances.get(currentUnit).get(0).getMaxValue(YEAR_2010))); +// } +// +// private BalanceEmployeeDto getBalanceDto(BalanceValueDto balanceEmployeeValueDto, double deficit) { +// BalanceEmployeeDto dto = new BalanceEmployeeDto(BalanceEmployeeDto.BalanceEmployeeKey.DEFICIT); +// dto.setValue(balanceEmployeeValueDto.getPeriod().getYear(), 1, balanceEmployeeValueDto.getWorkType(), deficit); +// dto.setValue(YEAR_2011, 1, balanceEmployeeValueDto.getWorkType(), 100.0); +// return dto; +// } +} diff --git a/src/test/java/XlsDocumentBuilderTest.java b/src/test/java/XlsDocumentBuilderTest.java new file mode 100644 index 0000000..8474fcf --- /dev/null +++ b/src/test/java/XlsDocumentBuilderTest.java @@ -0,0 +1,126 @@ +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.assertj.core.util.Files; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import ru.ulstu.core.error.XlsParseException; +import ru.ulstu.core.service.XlsDocumentBuilder; + +import java.io.*; + +public class XlsDocumentBuilderTest { + private final static String TEST_FILE_NAME = "testDoc.xlsx"; + private final static String TEST_XLS_FILE_NAME = "testDoc.xls"; + private final static String CELL_TEST_VALUE = "cell test value"; + + @Before + public void before() { + Files.delete(new File(TEST_FILE_NAME)); + Files.delete(new File(TEST_XLS_FILE_NAME)); + } + + @Test + public void documentSaveTest() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile); + xlsDocumentBuilder.save(); + File savedFile = new File(TEST_FILE_NAME); + XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(savedFile)); + Assert.assertEquals(workbook.getActiveSheetIndex(), 0); + } + + @Test + public void insertSheetTest() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile) + .insertNewSheet("new sheet"); + Assert.assertEquals(xlsDocumentBuilder.getSheetCount(), 2); + Assert.assertEquals(xlsDocumentBuilder.getActiveSheetIndex(), 1); + } + + @Test + public void openExistingFile() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + new XlsDocumentBuilder(xlsFile) + .insertNewSheet("new sheet") + .save(); + + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile); + Assert.assertEquals(xlsDocumentBuilder.getSheetCount(), 2); + } + + @Test + public void openExistingXlsFile() throws IOException, XlsParseException { + File xlsFile = new File(TEST_XLS_FILE_NAME); + Workbook workbook = new HSSFWorkbook(); + workbook.createSheet("123"); + OutputStream out = new FileOutputStream(xlsFile); + workbook.write(out); + + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile); + Assert.assertEquals(xlsDocumentBuilder.getSheetCount(), 1); + } + + @Test + public void openWrongXlsFile() throws IOException, XlsParseException { + File xlsFile = new File(TEST_XLS_FILE_NAME); + OutputStream out = new FileOutputStream(xlsFile); + out.write(1); + out.close(); + + boolean wrongDocument = false; + try { + new XlsDocumentBuilder(xlsFile); + } catch (XlsParseException ex) { + wrongDocument = true; + } + Assert.assertTrue(wrongDocument); + } + + @Test + public void setActiveSheetTest() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile) + .insertNewSheet("new sheet") + .setActiveSheet(0); + Assert.assertEquals(xlsDocumentBuilder.getActiveSheetIndex(), 0); + } + + @Test + public void getRowCountTest() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile) + .insertNewSheet("new sheet"); + Assert.assertEquals(xlsDocumentBuilder.getRowCount(), 0); + xlsDocumentBuilder.setCellValue(10, 10, CELL_TEST_VALUE); + Assert.assertEquals(xlsDocumentBuilder.getRowCount(), 10); + } + + @Test + public void getColumnCountTest() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile) + .insertNewSheet("new sheet"); + Assert.assertEquals(xlsDocumentBuilder.getColumnCount(), 0); + xlsDocumentBuilder.setCellValue(10, 10, CELL_TEST_VALUE); + Assert.assertEquals(xlsDocumentBuilder.getColumnCount(), 10); + } + + @Test + public void getCellAsStringTest() throws IOException, XlsParseException { + File xlsFile = new File(TEST_FILE_NAME); + XlsDocumentBuilder xlsDocumentBuilder = new XlsDocumentBuilder(xlsFile) + .insertNewSheet("new sheet"); + xlsDocumentBuilder.setCellValue(10, 10, CELL_TEST_VALUE); + Assert.assertEquals(xlsDocumentBuilder.getCellAsString(10, 10), CELL_TEST_VALUE); + } + + @After + public void after() { + Files.delete(new File(TEST_FILE_NAME)); + Files.delete(new File(TEST_XLS_FILE_NAME)); + } +}