From 78f87104d66ae0f9d01ba052e58c02cdbec6973f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 21 Mar 2017 08:23:41 -0300 Subject: [PATCH] BAEL-137 Intro do JHipster (#1427) * refactor: Reorder tests without lambda Moves inner implementations of Answer and ArgumentMatcher to the top of the test classes. Also changes the lambda expression to a regular "pre java 8" expression in one of the tests. Resolves: BAEL-632 * feat: Create basic Monolithic JHipster project Commit just after creating a JHipster project, before making any modifications. Resolves: BAEL-137 * chore: Change the artifactId and name of the project From baeldung to jhipster-monolithic and JHipster Monolithic Application Relates to: BAEL-137 * feat: Create entities Post and Comment Relates to: BAEL-137 * feat: Fix Gatling configuration in pom.xml Relates to: BAEL-137 * feat: Add files for Continuous Integration Relates to: BAEL-137 * feat: Change pom.xml to conform to Baeldung standards - moved the element to the bottom of the file - excluded integration tests in the default surefire configuration - added a new profile, called integration, and added the integration tests there - added Java 8 in the and tags, under maven-compiler solves: BAEL-137 * chore: Add jhipster module to parent pom --- jhipster/.editorconfig | 24 + jhipster/.gitattributes | 22 + jhipster/.gitignore | 143 +++ jhipster/.gitlab-ci.yml | 56 + jhipster/.jhipster/Comment.json | 39 + jhipster/.jhipster/Post.json | 52 + jhipster/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 49502 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + jhipster/.travis.yml | 41 + jhipster/.yo-rc.json | 36 + jhipster/Jenkinsfile | 50 + jhipster/README.md | 153 +++ jhipster/angular-cli.json | 55 + jhipster/circle.yml | 25 + jhipster/mvnw | 233 ++++ jhipster/mvnw.cmd | 145 +++ jhipster/package.json | 112 ++ jhipster/pom.xml | 1034 +++++++++++++++++ jhipster/postcss.config.js | 3 + jhipster/src/main/docker/Dockerfile | 13 + jhipster/src/main/docker/app.yml | 14 + jhipster/src/main/docker/mysql.yml | 13 + jhipster/src/main/docker/sonar.yml | 7 + .../java/com/baeldung/ApplicationWebXml.java | 21 + .../main/java/com/baeldung/BaeldungApp.java | 84 ++ .../baeldung/aop/logging/LoggingAspect.java | 79 ++ .../config/ApplicationProperties.java | 15 + .../baeldung/config/AsyncConfiguration.java | 46 + .../baeldung/config/CacheConfiguration.java | 48 + .../config/CloudDatabaseConfiguration.java | 23 + .../java/com/baeldung/config/Constants.java | 16 + .../config/DatabaseConfiguration.java | 75 ++ .../config/DateTimeFormatConfiguration.java | 17 + .../baeldung/config/DefaultProfileUtil.java | 48 + .../baeldung/config/LocaleConfiguration.java | 35 + .../config/LoggingAspectConfiguration.java | 19 + .../baeldung/config/LoggingConfiguration.java | 109 ++ .../baeldung/config/MetricsConfiguration.java | 94 ++ .../config/SecurityConfiguration.java | 127 ++ .../config/ThymeleafConfiguration.java | 26 + .../com/baeldung/config/WebConfigurer.java | 186 +++ .../config/audit/AuditEventConverter.java | 91 ++ .../baeldung/config/audit/package-info.java | 4 + .../com/baeldung/config/package-info.java | 4 + .../domain/AbstractAuditingEntity.java | 80 ++ .../java/com/baeldung/domain/Authority.java | 66 ++ .../java/com/baeldung/domain/Comment.java | 114 ++ .../baeldung/domain/PersistentAuditEvent.java | 78 ++ .../main/java/com/baeldung/domain/Post.java | 133 +++ .../main/java/com/baeldung/domain/User.java | 235 ++++ .../com/baeldung/domain/package-info.java | 4 + .../repository/AuthorityRepository.java | 11 + .../repository/CommentRepository.java | 15 + .../CustomAuditEventRepository.java | 81 ++ .../PersistenceAuditEventRepository.java | 26 + .../baeldung/repository/PostRepository.java | 18 + .../baeldung/repository/UserRepository.java | 37 + .../com/baeldung/repository/package-info.java | 4 + .../security/AuthoritiesConstants.java | 16 + .../security/DomainUserDetailsService.java | 51 + .../com/baeldung/security/SecurityUtils.java | 68 ++ .../security/SpringSecurityAuditorAware.java | 19 + .../security/UserNotActivatedException.java | 19 + .../baeldung/security/jwt/JWTConfigurer.java | 23 + .../com/baeldung/security/jwt/JWTFilter.java | 58 + .../baeldung/security/jwt/TokenProvider.java | 109 ++ .../com/baeldung/security/package-info.java | 4 + .../baeldung/service/AuditEventService.java | 50 + .../com/baeldung/service/MailService.java | 108 ++ .../com/baeldung/service/UserService.java | 228 ++++ .../com/baeldung/service/dto/UserDTO.java | 167 +++ .../baeldung/service/dto/package-info.java | 4 + .../baeldung/service/mapper/UserMapper.java | 55 + .../baeldung/service/mapper/package-info.java | 4 + .../com/baeldung/service/package-info.java | 4 + .../com/baeldung/service/util/RandomUtil.java | 41 + .../baeldung/web/rest/AccountResource.java | 202 ++++ .../com/baeldung/web/rest/AuditResource.java | 76 ++ .../baeldung/web/rest/CommentResource.java | 129 ++ .../java/com/baeldung/web/rest/JWTToken.java | 24 + .../com/baeldung/web/rest/LogsResource.java | 39 + .../com/baeldung/web/rest/PostResource.java | 129 ++ .../web/rest/ProfileInfoResource.java | 69 ++ .../baeldung/web/rest/UserJWTController.java | 60 + .../com/baeldung/web/rest/UserResource.java | 187 +++ .../errors/CustomParameterizedException.java | 34 + .../web/rest/errors/ErrorConstants.java | 14 + .../com/baeldung/web/rest/errors/ErrorVM.java | 52 + .../web/rest/errors/ExceptionTranslator.java | 85 ++ .../web/rest/errors/FieldErrorVM.java | 33 + .../web/rest/errors/ParameterizedErrorVM.java | 27 + .../com/baeldung/web/rest/package-info.java | 4 + .../baeldung/web/rest/util/HeaderUtil.java | 45 + .../web/rest/util/PaginationUtil.java | 47 + .../web/rest/vm/KeyAndPasswordVM.java | 27 + .../com/baeldung/web/rest/vm/LoggerVM.java | 48 + .../com/baeldung/web/rest/vm/LoginVM.java | 55 + .../baeldung/web/rest/vm/ManagedUserVM.java | 45 + .../baeldung/web/rest/vm/package-info.java | 4 + .../src/main/resources/.h2.server.properties | 5 + jhipster/src/main/resources/banner.txt | 10 + .../main/resources/config/application-dev.yml | 130 +++ .../resources/config/application-prod.yml | 132 +++ .../src/main/resources/config/application.yml | 106 ++ .../config/liquibase/authorities.csv | 3 + .../00000000000000_initial_schema.xml | 149 +++ .../20170316223211_added_entity_Post.xml | 45 + ...16223211_added_entity_constraints_Post.xml | 18 + .../20170316224021_added_entity_Comment.xml | 41 + ...24021_added_entity_constraints_Comment.xml | 18 + .../resources/config/liquibase/master.xml | 14 + .../main/resources/config/liquibase/users.csv | 5 + .../config/liquibase/users_authorities.csv | 6 + .../main/resources/i18n/messages.properties | 22 + .../resources/i18n/messages_en.properties | 22 + .../src/main/resources/logback-spring.xml | 66 ++ .../main/resources/mails/activationEmail.html | 24 + .../main/resources/mails/creationEmail.html | 24 + .../resources/mails/passwordResetEmail.html | 24 + .../src/main/resources/templates/error.html | 162 +++ jhipster/src/main/webapp/404.html | 60 + .../main/webapp/app/account/account.module.ts | 45 + .../main/webapp/app/account/account.route.ts | 26 + .../account/activate/activate.component.html | 19 + .../account/activate/activate.component.ts | 42 + .../app/account/activate/activate.route.ts | 14 + .../app/account/activate/activate.service.ts | 18 + jhipster/src/main/webapp/app/account/index.ts | 19 + .../password-reset-finish.component.html | 77 ++ .../finish/password-reset-finish.component.ts | 65 ++ .../finish/password-reset-finish.route.ts | 14 + .../finish/password-reset-finish.service.ts | 13 + .../init/password-reset-init.component.html | 47 + .../init/password-reset-init.component.ts | 49 + .../init/password-reset-init.route.ts | 14 + .../init/password-reset-init.service.ts | 13 + .../password-strength-bar.component.ts | 89 ++ .../password/password-strength-bar.scss | 23 + .../account/password/password.component.html | 65 ++ .../account/password/password.component.ts | 49 + .../app/account/password/password.route.ts | 14 + .../app/account/password/password.service.ts | 13 + .../account/register/register.component.html | 122 ++ .../account/register/register.component.ts | 73 ++ .../app/account/register/register.route.ts | 14 + .../app/account/register/register.service.ts | 13 + .../account/settings/settings.component.html | 86 ++ .../account/settings/settings.component.ts | 63 + .../app/account/settings/settings.route.ts | 14 + .../src/main/webapp/app/admin/admin.module.ts | 73 ++ .../src/main/webapp/app/admin/admin.route.ts | 36 + .../app/admin/audits/audit-data.model.ts | 6 + .../webapp/app/admin/audits/audit.model.ts | 10 + .../app/admin/audits/audits.component.html | 45 + .../app/admin/audits/audits.component.ts | 99 ++ .../webapp/app/admin/audits/audits.route.ts | 12 + .../webapp/app/admin/audits/audits.service.ts | 23 + .../configuration.component.html | 46 + .../configuration/configuration.component.ts | 48 + .../configuration/configuration.route.ts | 12 + .../configuration/configuration.service.ts | 53 + .../webapp/app/admin/docs/docs.component.html | 2 + .../webapp/app/admin/docs/docs.component.ts | 14 + .../main/webapp/app/admin/docs/docs.route.ts | 12 + .../admin/health/health-modal.component.html | 36 + .../admin/health/health-modal.component.ts | 37 + .../app/admin/health/health.component.html | 34 + .../app/admin/health/health.component.ts | 69 ++ .../webapp/app/admin/health/health.route.ts | 12 + .../webapp/app/admin/health/health.service.ts | 140 +++ jhipster/src/main/webapp/app/admin/index.ts | 29 + .../main/webapp/app/admin/logs/log.model.ts | 6 + .../webapp/app/admin/logs/logs.component.html | 27 + .../webapp/app/admin/logs/logs.component.ts | 38 + .../main/webapp/app/admin/logs/logs.route.ts | 12 + .../webapp/app/admin/logs/logs.service.ts | 18 + .../metrics/metrics-modal.component.html | 56 + .../admin/metrics/metrics-modal.component.ts | 48 + .../app/admin/metrics/metrics.component.html | 253 ++++ .../app/admin/metrics/metrics.component.ts | 74 ++ .../webapp/app/admin/metrics/metrics.route.ts | 12 + .../app/admin/metrics/metrics.service.ts | 17 + ...er-management-delete-dialog.component.html | 19 + ...user-management-delete-dialog.component.ts | 63 + .../user-management-detail.component.html | 44 + .../user-management-detail.component.ts | 40 + .../user-management-dialog.component.html | 112 ++ .../user-management-dialog.component.ts | 89 ++ .../user-management.component.html | 81 ++ .../user-management.component.ts | 131 +++ .../user-management/user-management.route.ts | 77 ++ .../user-management/user-modal.service.ts | 42 + jhipster/src/main/webapp/app/app.constants.ts | 7 + jhipster/src/main/webapp/app/app.main.ts | 11 + jhipster/src/main/webapp/app/app.module.ts | 58 + jhipster/src/main/webapp/app/app.route.ts | 9 + .../webapp/app/blocks/config/prod.config.ts | 9 + .../blocks/config/uib-pagination.config.ts | 13 + .../interceptor/auth-expired.interceptor.ts | 34 + .../blocks/interceptor/auth.interceptor.ts | 27 + .../interceptor/errorhandler.interceptor.ts | 24 + .../app/blocks/interceptor/http.provider.ts | 45 + .../interceptor/notification.interceptor.ts | 33 + .../comment-delete-dialog.component.html | 19 + .../comment-delete-dialog.component.ts | 67 ++ .../comment/comment-detail.component.html | 35 + .../comment/comment-detail.component.ts | 43 + .../comment/comment-dialog.component.html | 76 ++ .../comment/comment-dialog.component.ts | 107 ++ .../entities/comment/comment-popup.service.ts | 50 + .../entities/comment/comment.component.html | 64 + .../app/entities/comment/comment.component.ts | 110 ++ .../app/entities/comment/comment.model.ts | 10 + .../app/entities/comment/comment.module.ts | 50 + .../app/entities/comment/comment.route.ts | 61 + .../app/entities/comment/comment.service.ts | 78 ++ .../main/webapp/app/entities/comment/index.ts | 8 + .../main/webapp/app/entities/entity.module.ts | 18 + .../main/webapp/app/entities/post/index.ts | 8 + .../post/post-delete-dialog.component.html | 19 + .../post/post-delete-dialog.component.ts | 67 ++ .../entities/post/post-detail.component.html | 37 + .../entities/post/post-detail.component.ts | 43 + .../entities/post/post-dialog.component.html | 96 ++ .../entities/post/post-dialog.component.ts | 107 ++ .../app/entities/post/post-popup.service.ts | 50 + .../app/entities/post/post.component.html | 64 + .../app/entities/post/post.component.ts | 110 ++ .../webapp/app/entities/post/post.model.ts | 11 + .../webapp/app/entities/post/post.module.ts | 52 + .../webapp/app/entities/post/post.route.ts | 61 + .../webapp/app/entities/post/post.service.ts | 78 ++ .../main/webapp/app/home/home.component.html | 42 + .../main/webapp/app/home/home.component.ts | 50 + .../src/main/webapp/app/home/home.module.ts | 23 + .../src/main/webapp/app/home/home.route.ts | 14 + jhipster/src/main/webapp/app/home/home.scss | 26 + jhipster/src/main/webapp/app/home/index.ts | 3 + .../app/layouts/error/error.component.html | 17 + .../app/layouts/error/error.component.ts | 20 + .../webapp/app/layouts/error/error.route.ts | 25 + .../app/layouts/footer/footer.component.html | 4 + .../app/layouts/footer/footer.component.ts | 7 + jhipster/src/main/webapp/app/layouts/index.ts | 10 + .../app/layouts/layout-routing.module.ts | 20 + .../app/layouts/main/main.component.html | 11 + .../webapp/app/layouts/main/main.component.ts | 47 + .../layouts/navbar/active-menu.directive.ts | 26 + .../app/layouts/navbar/navbar.component.html | 168 +++ .../app/layouts/navbar/navbar.component.ts | 81 ++ .../webapp/app/layouts/navbar/navbar.scss | 70 ++ .../layouts/profiles/page-ribbon.component.ts | 25 + .../app/layouts/profiles/page-ribbon.scss | 32 + .../layouts/profiles/profile-info.model.ts | 6 + .../app/layouts/profiles/profile.service.ts | 26 + jhipster/src/main/webapp/app/polyfills.ts | 3 + .../app/shared/alert/alert-error.component.ts | 101 ++ .../app/shared/alert/alert.component.ts | 26 + .../webapp/app/shared/auth/account.service.ts | 16 + .../app/shared/auth/auth-jwt.service.ts | 61 + .../webapp/app/shared/auth/auth.service.ts | 65 ++ .../webapp/app/shared/auth/csrf.service.ts | 13 + .../auth/has-any-authority.directive.ts | 42 + .../app/shared/auth/principal.service.ts | 93 ++ .../app/shared/auth/state-storage.service.ts | 40 + .../shared/auth/user-route-access-service.ts | 15 + .../shared/constants/pagination.constants.ts | 1 + jhipster/src/main/webapp/app/shared/index.ts | 23 + .../app/shared/language/language.constants.ts | 8 + .../app/shared/language/language.helper.ts | 53 + .../app/shared/language/language.pipe.ts | 42 + .../app/shared/login/login-modal.service.ts | 26 + .../app/shared/login/login.component.html | 46 + .../app/shared/login/login.component.ts | 90 ++ .../webapp/app/shared/login/login.service.ts | 45 + .../webapp/app/shared/shared-common.module.ts | 47 + .../webapp/app/shared/shared-libs.module.ts | 27 + .../main/webapp/app/shared/shared.module.ts | 53 + .../webapp/app/shared/user/account.model.ts | 12 + .../main/webapp/app/shared/user/user.model.ts | 44 + .../webapp/app/shared/user/user.service.ts | 45 + jhipster/src/main/webapp/app/vendor.ts | 3 + .../main/webapp/content/images/hipster.png | Bin 0 -> 9499 bytes .../main/webapp/content/images/hipster2x.png | Bin 0 -> 18872 bytes .../webapp/content/images/logo-jhipster.png | Bin 0 -> 4459 bytes .../src/main/webapp/content/scss/global.scss | 211 ++++ .../src/main/webapp/content/scss/vendor.scss | 10 + jhipster/src/main/webapp/favicon.ico | Bin 0 -> 5430 bytes .../src/main/webapp/i18n/en/activate.json | 9 + jhipster/src/main/webapp/i18n/en/audits.json | 27 + jhipster/src/main/webapp/i18n/en/comment.json | 23 + .../main/webapp/i18n/en/configuration.json | 10 + jhipster/src/main/webapp/i18n/en/error.json | 6 + jhipster/src/main/webapp/i18n/en/gateway.json | 15 + jhipster/src/main/webapp/i18n/en/global.json | 133 +++ jhipster/src/main/webapp/i18n/en/health.json | 27 + jhipster/src/main/webapp/i18n/en/home.json | 19 + jhipster/src/main/webapp/i18n/en/login.json | 19 + jhipster/src/main/webapp/i18n/en/logs.json | 11 + jhipster/src/main/webapp/i18n/en/metrics.json | 101 ++ .../src/main/webapp/i18n/en/password.json | 12 + jhipster/src/main/webapp/i18n/en/post.json | 24 + .../src/main/webapp/i18n/en/register.json | 24 + jhipster/src/main/webapp/i18n/en/reset.json | 27 + .../src/main/webapp/i18n/en/sessions.json | 15 + .../src/main/webapp/i18n/en/settings.json | 32 + .../main/webapp/i18n/en/user-management.json | 30 + jhipster/src/main/webapp/index.html | 27 + jhipster/src/main/webapp/robots.txt | 11 + .../webapp/swagger-ui/images/throbber.gif | Bin 0 -> 9257 bytes .../src/main/webapp/swagger-ui/index.html | 177 +++ jhipster/src/test/gatling/conf/gatling.conf | 131 +++ jhipster/src/test/gatling/conf/logback.xml | 22 + .../simulations/CommentGatlingTest.scala | 92 ++ .../gatling/simulations/PostGatlingTest.scala | 92 ++ .../security/SecurityUtilsUnitTest.java | 50 + .../security/jwt/TokenProviderTest.java | 107 ++ .../baeldung/service/UserServiceIntTest.java | 129 ++ .../web/rest/AccountResourceIntTest.java | 395 +++++++ .../web/rest/AuditResourceIntTest.java | 147 +++ .../web/rest/CommentResourceIntTest.java | 279 +++++ .../web/rest/LogsResourceIntTest.java | 59 + .../web/rest/PostResourceIntTest.java | 306 +++++ .../web/rest/ProfileInfoResourceIntTest.java | 86 ++ .../java/com/baeldung/web/rest/TestUtil.java | 120 ++ .../web/rest/UserResourceIntTest.java | 522 +++++++++ .../javascript/e2e/account/account.spec.ts | 108 ++ .../e2e/admin/administration.spec.ts | 80 ++ .../javascript/e2e/entities/comment.spec.ts | 49 + .../test/javascript/e2e/entities/post.spec.ts | 49 + jhipster/src/test/javascript/karma.conf.js | 126 ++ .../src/test/javascript/protractor.conf.js | 48 + .../activate/activate.component.spec.ts | 84 ++ .../password-reset-finish.component.spec.ts | 79 ++ .../password-reset-init.component.spec.ts | 115 ++ .../password-strength-bar.component.spec.ts | 53 + .../password/password.component.spec.ts | 92 ++ .../register/register.component.spec.ts | 138 +++ .../settings/settings.component.spec.ts | 103 ++ .../app/admin/audits/audits.component.spec.ts | 82 ++ .../app/admin/health/health.component.spec.ts | 295 +++++ .../comment/comment-detail.component.spec.ts | 79 ++ .../post/post-detail.component.spec.ts | 79 ++ jhipster/src/test/javascript/spec/entry.ts | 19 + .../spec/helpers/mock-account.service.ts | 26 + .../spec/helpers/mock-language.service.ts | 26 + .../spec/helpers/mock-principal.service.ts | 20 + .../spec/helpers/mock-route.service.ts | 15 + .../test/javascript/spec/helpers/spyobject.ts | 69 ++ .../src/test/javascript/spec/test.module.ts | 24 + .../src/test/resources/config/application.yml | 96 ++ jhipster/src/test/resources/logback-test.xml | 15 + jhipster/tsconfig.json | 22 + jhipster/tslint.json | 107 ++ jhipster/webpack/webpack.common.js | 117 ++ jhipster/webpack/webpack.dev.js | 65 ++ jhipster/webpack/webpack.prod.js | 22 + jhipster/webpack/webpack.vendor.js | 63 + .../ArgumentMatcherWithoutLambdaUnitTest.java | 20 +- .../CustomAnswerWithoutLambdaUnitTest.java | 25 +- pom.xml | 1 + 361 files changed, 20698 insertions(+), 21 deletions(-) create mode 100644 jhipster/.editorconfig create mode 100644 jhipster/.gitattributes create mode 100644 jhipster/.gitignore create mode 100644 jhipster/.gitlab-ci.yml create mode 100644 jhipster/.jhipster/Comment.json create mode 100644 jhipster/.jhipster/Post.json create mode 100644 jhipster/.mvn/wrapper/maven-wrapper.jar create mode 100644 jhipster/.mvn/wrapper/maven-wrapper.properties create mode 100644 jhipster/.travis.yml create mode 100644 jhipster/.yo-rc.json create mode 100644 jhipster/Jenkinsfile create mode 100644 jhipster/README.md create mode 100644 jhipster/angular-cli.json create mode 100644 jhipster/circle.yml create mode 100755 jhipster/mvnw create mode 100644 jhipster/mvnw.cmd create mode 100644 jhipster/package.json create mode 100644 jhipster/pom.xml create mode 100644 jhipster/postcss.config.js create mode 100644 jhipster/src/main/docker/Dockerfile create mode 100644 jhipster/src/main/docker/app.yml create mode 100644 jhipster/src/main/docker/mysql.yml create mode 100644 jhipster/src/main/docker/sonar.yml create mode 100644 jhipster/src/main/java/com/baeldung/ApplicationWebXml.java create mode 100644 jhipster/src/main/java/com/baeldung/BaeldungApp.java create mode 100644 jhipster/src/main/java/com/baeldung/aop/logging/LoggingAspect.java create mode 100644 jhipster/src/main/java/com/baeldung/config/ApplicationProperties.java create mode 100644 jhipster/src/main/java/com/baeldung/config/AsyncConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/CacheConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/CloudDatabaseConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/Constants.java create mode 100644 jhipster/src/main/java/com/baeldung/config/DatabaseConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/DateTimeFormatConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/DefaultProfileUtil.java create mode 100644 jhipster/src/main/java/com/baeldung/config/LocaleConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/LoggingAspectConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/LoggingConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/MetricsConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/SecurityConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/ThymeleafConfiguration.java create mode 100644 jhipster/src/main/java/com/baeldung/config/WebConfigurer.java create mode 100644 jhipster/src/main/java/com/baeldung/config/audit/AuditEventConverter.java create mode 100644 jhipster/src/main/java/com/baeldung/config/audit/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/config/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/AbstractAuditingEntity.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/Authority.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/Comment.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/PersistentAuditEvent.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/Post.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/User.java create mode 100644 jhipster/src/main/java/com/baeldung/domain/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/AuthorityRepository.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/CommentRepository.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/CustomAuditEventRepository.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/PersistenceAuditEventRepository.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/PostRepository.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/UserRepository.java create mode 100644 jhipster/src/main/java/com/baeldung/repository/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/security/AuthoritiesConstants.java create mode 100644 jhipster/src/main/java/com/baeldung/security/DomainUserDetailsService.java create mode 100644 jhipster/src/main/java/com/baeldung/security/SecurityUtils.java create mode 100644 jhipster/src/main/java/com/baeldung/security/SpringSecurityAuditorAware.java create mode 100644 jhipster/src/main/java/com/baeldung/security/UserNotActivatedException.java create mode 100644 jhipster/src/main/java/com/baeldung/security/jwt/JWTConfigurer.java create mode 100644 jhipster/src/main/java/com/baeldung/security/jwt/JWTFilter.java create mode 100644 jhipster/src/main/java/com/baeldung/security/jwt/TokenProvider.java create mode 100644 jhipster/src/main/java/com/baeldung/security/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/service/AuditEventService.java create mode 100644 jhipster/src/main/java/com/baeldung/service/MailService.java create mode 100644 jhipster/src/main/java/com/baeldung/service/UserService.java create mode 100644 jhipster/src/main/java/com/baeldung/service/dto/UserDTO.java create mode 100644 jhipster/src/main/java/com/baeldung/service/dto/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/service/mapper/UserMapper.java create mode 100644 jhipster/src/main/java/com/baeldung/service/mapper/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/service/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/service/util/RandomUtil.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/AccountResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/AuditResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/CommentResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/JWTToken.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/LogsResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/PostResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/ProfileInfoResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/UserJWTController.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/UserResource.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/errors/CustomParameterizedException.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorConstants.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/errors/ExceptionTranslator.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/errors/FieldErrorVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/errors/ParameterizedErrorVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/package-info.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/util/HeaderUtil.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/util/PaginationUtil.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/vm/KeyAndPasswordVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/vm/LoggerVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/vm/LoginVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/vm/ManagedUserVM.java create mode 100644 jhipster/src/main/java/com/baeldung/web/rest/vm/package-info.java create mode 100644 jhipster/src/main/resources/.h2.server.properties create mode 100644 jhipster/src/main/resources/banner.txt create mode 100644 jhipster/src/main/resources/config/application-dev.yml create mode 100644 jhipster/src/main/resources/config/application-prod.yml create mode 100644 jhipster/src/main/resources/config/application.yml create mode 100644 jhipster/src/main/resources/config/liquibase/authorities.csv create mode 100644 jhipster/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml create mode 100644 jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_Post.xml create mode 100644 jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_constraints_Post.xml create mode 100644 jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_Comment.xml create mode 100644 jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_constraints_Comment.xml create mode 100644 jhipster/src/main/resources/config/liquibase/master.xml create mode 100644 jhipster/src/main/resources/config/liquibase/users.csv create mode 100644 jhipster/src/main/resources/config/liquibase/users_authorities.csv create mode 100644 jhipster/src/main/resources/i18n/messages.properties create mode 100644 jhipster/src/main/resources/i18n/messages_en.properties create mode 100644 jhipster/src/main/resources/logback-spring.xml create mode 100644 jhipster/src/main/resources/mails/activationEmail.html create mode 100644 jhipster/src/main/resources/mails/creationEmail.html create mode 100644 jhipster/src/main/resources/mails/passwordResetEmail.html create mode 100644 jhipster/src/main/resources/templates/error.html create mode 100644 jhipster/src/main/webapp/404.html create mode 100644 jhipster/src/main/webapp/app/account/account.module.ts create mode 100644 jhipster/src/main/webapp/app/account/account.route.ts create mode 100644 jhipster/src/main/webapp/app/account/activate/activate.component.html create mode 100644 jhipster/src/main/webapp/app/account/activate/activate.component.ts create mode 100644 jhipster/src/main/webapp/app/account/activate/activate.route.ts create mode 100644 jhipster/src/main/webapp/app/account/activate/activate.service.ts create mode 100644 jhipster/src/main/webapp/app/account/index.ts create mode 100644 jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html create mode 100644 jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts create mode 100644 jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts create mode 100644 jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts create mode 100644 jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html create mode 100644 jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts create mode 100644 jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts create mode 100644 jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts create mode 100644 jhipster/src/main/webapp/app/account/password/password-strength-bar.component.ts create mode 100644 jhipster/src/main/webapp/app/account/password/password-strength-bar.scss create mode 100644 jhipster/src/main/webapp/app/account/password/password.component.html create mode 100644 jhipster/src/main/webapp/app/account/password/password.component.ts create mode 100644 jhipster/src/main/webapp/app/account/password/password.route.ts create mode 100644 jhipster/src/main/webapp/app/account/password/password.service.ts create mode 100644 jhipster/src/main/webapp/app/account/register/register.component.html create mode 100644 jhipster/src/main/webapp/app/account/register/register.component.ts create mode 100644 jhipster/src/main/webapp/app/account/register/register.route.ts create mode 100644 jhipster/src/main/webapp/app/account/register/register.service.ts create mode 100644 jhipster/src/main/webapp/app/account/settings/settings.component.html create mode 100644 jhipster/src/main/webapp/app/account/settings/settings.component.ts create mode 100644 jhipster/src/main/webapp/app/account/settings/settings.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/admin.module.ts create mode 100644 jhipster/src/main/webapp/app/admin/admin.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/audits/audit-data.model.ts create mode 100644 jhipster/src/main/webapp/app/admin/audits/audit.model.ts create mode 100644 jhipster/src/main/webapp/app/admin/audits/audits.component.html create mode 100644 jhipster/src/main/webapp/app/admin/audits/audits.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/audits/audits.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/audits/audits.service.ts create mode 100644 jhipster/src/main/webapp/app/admin/configuration/configuration.component.html create mode 100644 jhipster/src/main/webapp/app/admin/configuration/configuration.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/configuration/configuration.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/configuration/configuration.service.ts create mode 100644 jhipster/src/main/webapp/app/admin/docs/docs.component.html create mode 100644 jhipster/src/main/webapp/app/admin/docs/docs.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/docs/docs.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/health/health-modal.component.html create mode 100644 jhipster/src/main/webapp/app/admin/health/health-modal.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/health/health.component.html create mode 100644 jhipster/src/main/webapp/app/admin/health/health.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/health/health.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/health/health.service.ts create mode 100644 jhipster/src/main/webapp/app/admin/index.ts create mode 100644 jhipster/src/main/webapp/app/admin/logs/log.model.ts create mode 100644 jhipster/src/main/webapp/app/admin/logs/logs.component.html create mode 100644 jhipster/src/main/webapp/app/admin/logs/logs.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/logs/logs.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/logs/logs.service.ts create mode 100644 jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.html create mode 100644 jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/metrics/metrics.component.html create mode 100644 jhipster/src/main/webapp/app/admin/metrics/metrics.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/metrics/metrics.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/metrics/metrics.service.ts create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.html create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.html create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.html create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management.component.html create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management.component.ts create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-management.route.ts create mode 100644 jhipster/src/main/webapp/app/admin/user-management/user-modal.service.ts create mode 100644 jhipster/src/main/webapp/app/app.constants.ts create mode 100644 jhipster/src/main/webapp/app/app.main.ts create mode 100644 jhipster/src/main/webapp/app/app.module.ts create mode 100644 jhipster/src/main/webapp/app/app.route.ts create mode 100644 jhipster/src/main/webapp/app/blocks/config/prod.config.ts create mode 100644 jhipster/src/main/webapp/app/blocks/config/uib-pagination.config.ts create mode 100644 jhipster/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.ts create mode 100644 jhipster/src/main/webapp/app/blocks/interceptor/auth.interceptor.ts create mode 100644 jhipster/src/main/webapp/app/blocks/interceptor/errorhandler.interceptor.ts create mode 100644 jhipster/src/main/webapp/app/blocks/interceptor/http.provider.ts create mode 100644 jhipster/src/main/webapp/app/blocks/interceptor/notification.interceptor.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.html create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-detail.component.html create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-detail.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.html create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment-popup.service.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment.component.html create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment.model.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment.module.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment.route.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/comment.service.ts create mode 100644 jhipster/src/main/webapp/app/entities/comment/index.ts create mode 100644 jhipster/src/main/webapp/app/entities/entity.module.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/index.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.html create mode 100644 jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post-detail.component.html create mode 100644 jhipster/src/main/webapp/app/entities/post/post-detail.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post-dialog.component.html create mode 100644 jhipster/src/main/webapp/app/entities/post/post-dialog.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post-popup.service.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post.component.html create mode 100644 jhipster/src/main/webapp/app/entities/post/post.component.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post.model.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post.module.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post.route.ts create mode 100644 jhipster/src/main/webapp/app/entities/post/post.service.ts create mode 100644 jhipster/src/main/webapp/app/home/home.component.html create mode 100644 jhipster/src/main/webapp/app/home/home.component.ts create mode 100644 jhipster/src/main/webapp/app/home/home.module.ts create mode 100644 jhipster/src/main/webapp/app/home/home.route.ts create mode 100644 jhipster/src/main/webapp/app/home/home.scss create mode 100644 jhipster/src/main/webapp/app/home/index.ts create mode 100644 jhipster/src/main/webapp/app/layouts/error/error.component.html create mode 100644 jhipster/src/main/webapp/app/layouts/error/error.component.ts create mode 100644 jhipster/src/main/webapp/app/layouts/error/error.route.ts create mode 100644 jhipster/src/main/webapp/app/layouts/footer/footer.component.html create mode 100644 jhipster/src/main/webapp/app/layouts/footer/footer.component.ts create mode 100644 jhipster/src/main/webapp/app/layouts/index.ts create mode 100644 jhipster/src/main/webapp/app/layouts/layout-routing.module.ts create mode 100644 jhipster/src/main/webapp/app/layouts/main/main.component.html create mode 100644 jhipster/src/main/webapp/app/layouts/main/main.component.ts create mode 100644 jhipster/src/main/webapp/app/layouts/navbar/active-menu.directive.ts create mode 100644 jhipster/src/main/webapp/app/layouts/navbar/navbar.component.html create mode 100644 jhipster/src/main/webapp/app/layouts/navbar/navbar.component.ts create mode 100644 jhipster/src/main/webapp/app/layouts/navbar/navbar.scss create mode 100644 jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts create mode 100644 jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.scss create mode 100644 jhipster/src/main/webapp/app/layouts/profiles/profile-info.model.ts create mode 100644 jhipster/src/main/webapp/app/layouts/profiles/profile.service.ts create mode 100644 jhipster/src/main/webapp/app/polyfills.ts create mode 100644 jhipster/src/main/webapp/app/shared/alert/alert-error.component.ts create mode 100644 jhipster/src/main/webapp/app/shared/alert/alert.component.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/account.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/auth-jwt.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/auth.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/csrf.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/has-any-authority.directive.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/principal.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/state-storage.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/auth/user-route-access-service.ts create mode 100644 jhipster/src/main/webapp/app/shared/constants/pagination.constants.ts create mode 100644 jhipster/src/main/webapp/app/shared/index.ts create mode 100644 jhipster/src/main/webapp/app/shared/language/language.constants.ts create mode 100644 jhipster/src/main/webapp/app/shared/language/language.helper.ts create mode 100644 jhipster/src/main/webapp/app/shared/language/language.pipe.ts create mode 100644 jhipster/src/main/webapp/app/shared/login/login-modal.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/login/login.component.html create mode 100644 jhipster/src/main/webapp/app/shared/login/login.component.ts create mode 100644 jhipster/src/main/webapp/app/shared/login/login.service.ts create mode 100644 jhipster/src/main/webapp/app/shared/shared-common.module.ts create mode 100644 jhipster/src/main/webapp/app/shared/shared-libs.module.ts create mode 100644 jhipster/src/main/webapp/app/shared/shared.module.ts create mode 100644 jhipster/src/main/webapp/app/shared/user/account.model.ts create mode 100644 jhipster/src/main/webapp/app/shared/user/user.model.ts create mode 100644 jhipster/src/main/webapp/app/shared/user/user.service.ts create mode 100644 jhipster/src/main/webapp/app/vendor.ts create mode 100644 jhipster/src/main/webapp/content/images/hipster.png create mode 100644 jhipster/src/main/webapp/content/images/hipster2x.png create mode 100644 jhipster/src/main/webapp/content/images/logo-jhipster.png create mode 100644 jhipster/src/main/webapp/content/scss/global.scss create mode 100644 jhipster/src/main/webapp/content/scss/vendor.scss create mode 100644 jhipster/src/main/webapp/favicon.ico create mode 100644 jhipster/src/main/webapp/i18n/en/activate.json create mode 100644 jhipster/src/main/webapp/i18n/en/audits.json create mode 100644 jhipster/src/main/webapp/i18n/en/comment.json create mode 100644 jhipster/src/main/webapp/i18n/en/configuration.json create mode 100644 jhipster/src/main/webapp/i18n/en/error.json create mode 100644 jhipster/src/main/webapp/i18n/en/gateway.json create mode 100644 jhipster/src/main/webapp/i18n/en/global.json create mode 100644 jhipster/src/main/webapp/i18n/en/health.json create mode 100644 jhipster/src/main/webapp/i18n/en/home.json create mode 100644 jhipster/src/main/webapp/i18n/en/login.json create mode 100644 jhipster/src/main/webapp/i18n/en/logs.json create mode 100644 jhipster/src/main/webapp/i18n/en/metrics.json create mode 100644 jhipster/src/main/webapp/i18n/en/password.json create mode 100644 jhipster/src/main/webapp/i18n/en/post.json create mode 100644 jhipster/src/main/webapp/i18n/en/register.json create mode 100644 jhipster/src/main/webapp/i18n/en/reset.json create mode 100644 jhipster/src/main/webapp/i18n/en/sessions.json create mode 100644 jhipster/src/main/webapp/i18n/en/settings.json create mode 100644 jhipster/src/main/webapp/i18n/en/user-management.json create mode 100644 jhipster/src/main/webapp/index.html create mode 100644 jhipster/src/main/webapp/robots.txt create mode 100644 jhipster/src/main/webapp/swagger-ui/images/throbber.gif create mode 100644 jhipster/src/main/webapp/swagger-ui/index.html create mode 100644 jhipster/src/test/gatling/conf/gatling.conf create mode 100644 jhipster/src/test/gatling/conf/logback.xml create mode 100644 jhipster/src/test/gatling/simulations/CommentGatlingTest.scala create mode 100644 jhipster/src/test/gatling/simulations/PostGatlingTest.scala create mode 100644 jhipster/src/test/java/com/baeldung/security/SecurityUtilsUnitTest.java create mode 100644 jhipster/src/test/java/com/baeldung/security/jwt/TokenProviderTest.java create mode 100644 jhipster/src/test/java/com/baeldung/service/UserServiceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/AccountResourceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/AuditResourceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/CommentResourceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/LogsResourceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/PostResourceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/ProfileInfoResourceIntTest.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/TestUtil.java create mode 100644 jhipster/src/test/java/com/baeldung/web/rest/UserResourceIntTest.java create mode 100644 jhipster/src/test/javascript/e2e/account/account.spec.ts create mode 100644 jhipster/src/test/javascript/e2e/admin/administration.spec.ts create mode 100644 jhipster/src/test/javascript/e2e/entities/comment.spec.ts create mode 100644 jhipster/src/test/javascript/e2e/entities/post.spec.ts create mode 100644 jhipster/src/test/javascript/karma.conf.js create mode 100644 jhipster/src/test/javascript/protractor.conf.js create mode 100644 jhipster/src/test/javascript/spec/app/account/activate/activate.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/account/password-reset/finish/password-reset-finish.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/account/password-reset/init/password-reset-init.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/account/password/password-strength-bar.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/account/password/password.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/account/register/register.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/account/settings/settings.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/admin/audits/audits.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/admin/health/health.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/entities/comment/comment-detail.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/app/entities/post/post-detail.component.spec.ts create mode 100644 jhipster/src/test/javascript/spec/entry.ts create mode 100644 jhipster/src/test/javascript/spec/helpers/mock-account.service.ts create mode 100644 jhipster/src/test/javascript/spec/helpers/mock-language.service.ts create mode 100644 jhipster/src/test/javascript/spec/helpers/mock-principal.service.ts create mode 100644 jhipster/src/test/javascript/spec/helpers/mock-route.service.ts create mode 100644 jhipster/src/test/javascript/spec/helpers/spyobject.ts create mode 100644 jhipster/src/test/javascript/spec/test.module.ts create mode 100644 jhipster/src/test/resources/config/application.yml create mode 100644 jhipster/src/test/resources/logback-test.xml create mode 100644 jhipster/tsconfig.json create mode 100644 jhipster/tslint.json create mode 100644 jhipster/webpack/webpack.common.js create mode 100644 jhipster/webpack/webpack.dev.js create mode 100644 jhipster/webpack/webpack.prod.js create mode 100644 jhipster/webpack/webpack.vendor.js diff --git a/jhipster/.editorconfig b/jhipster/.editorconfig new file mode 100644 index 0000000000..a03599dd04 --- /dev/null +++ b/jhipster/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[{package,bower}.json] +indent_style = space +indent_size = 2 diff --git a/jhipster/.gitattributes b/jhipster/.gitattributes new file mode 100644 index 0000000000..2a13efa88f --- /dev/null +++ b/jhipster/.gitattributes @@ -0,0 +1,22 @@ +# All text files should have the "lf" (Unix) line endings +* text eol=lf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.java text +*.js text +*.css text +*.html text + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.jar binary +*.pdf binary +*.eot binary +*.ttf binary +*.gzip binary +*.gz binary +*.ai binary +*.eps binary +*.swf binary diff --git a/jhipster/.gitignore b/jhipster/.gitignore new file mode 100644 index 0000000000..c9f735a496 --- /dev/null +++ b/jhipster/.gitignore @@ -0,0 +1,143 @@ +###################### +# Project Specific +###################### +/src/main/webapp/content/css/main.css +/target/www/** +/src/test/javascript/coverage/ +/src/test/javascript/PhantomJS*/ + +###################### +# Node +###################### +/node/ +node_tmp/ +node_modules/ +npm-debug.log.* + +###################### +# SASS +###################### +.sass-cache/ + +###################### +# Eclipse +###################### +*.pydevproject +.project +.metadata +tmp/ +tmp/**/* +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath +.factorypath +/src/main/resources/rebel.xml + +# External tool builders +.externalToolBuilders/** + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +###################### +# Intellij +###################### +.idea/ +*.iml +*.iws +*.ipr +*.ids +*.orig + +###################### +# Visual Studio Code +###################### +.vscode/ + +###################### +# Maven +###################### +/log/ +/target/ + +###################### +# Gradle +###################### +.gradle/ +/build/ + +###################### +# Package Files +###################### +*.jar +*.war +*.ear +*.db + +###################### +# Windows +###################### +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + +###################### +# Mac OSX +###################### +.DS_Store +.svn + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +###################### +# Directories +###################### +/bin/ +/deploy/ + +###################### +# Logs +###################### +*.log + +###################### +# Others +###################### +*.class +*.*~ +*~ +.merge_file* + +###################### +# Gradle Wrapper +###################### +!gradle/wrapper/gradle-wrapper.jar + +###################### +# Maven Wrapper +###################### +!.mvn/wrapper/maven-wrapper.jar + +###################### +# ESLint +###################### +.eslintcache +/.apt_generated/ diff --git a/jhipster/.gitlab-ci.yml b/jhipster/.gitlab-ci.yml new file mode 100644 index 0000000000..1cf574251a --- /dev/null +++ b/jhipster/.gitlab-ci.yml @@ -0,0 +1,56 @@ + +cache: + key: "$CI_BUILD_REF_NAME" + paths: + - node_modules + - .maven +stages: + - build + - test + - package + +before_script: + - export MAVEN_USER_HOME=`pwd`/.maven + - chmod +x mvnw + - ./mvnw com.github.eirslett:frontend-maven-plugin:install-node-and-npm -DnodeVersion=v6.10.0 -DnpmVersion=4.3.0 + - ./mvnw com.github.eirslett:frontend-maven-plugin:npm + +maven-build: + stage: build + script: ./mvnw compile -Dmaven.repo.local=$MAVEN_USER_HOME + +maven-test: + stage: test + script: + - ./mvnw test -Dmaven.repo.local=$MAVEN_USER_HOME + artifacts: + paths: + - target/surefire-reports/* +maven-front-test: + stage: test + script: + - ./mvnw com.github.eirslett:frontend-maven-plugin:npm -Dfrontend.yarn.arguments=test + artifacts: + paths: + - target/test-results/karma/* +gatling-test: + stage: test + allow_failure: true + script: + - ./mvnw gatling:execute -Dmaven.repo.local=$MAVEN_USER_HOME + before_script: + - export MAVEN_USER_HOME=`pwd`/.maven + - chmod +x mvnw + - ./mvnw com.github.eirslett:frontend-maven-plugin:install-node-and-npm -DnodeVersion=v6.10.0 -DnpmVersion=4.3.0 + - ./mvnw com.github.eirslett:frontend-maven-plugin:npm + - ./mvnw & + artifacts: + paths: + - target/gatling/* +maven-package: + stage: package + script: + - ./mvnw package -Pprod -DskipTests -Dmaven.repo.local=$MAVEN_USER_HOME + artifacts: + paths: + - target/*.war diff --git a/jhipster/.jhipster/Comment.json b/jhipster/.jhipster/Comment.json new file mode 100644 index 0000000000..c6022daa60 --- /dev/null +++ b/jhipster/.jhipster/Comment.json @@ -0,0 +1,39 @@ +{ + "fluentMethods": true, + "relationships": [ + { + "relationshipName": "post", + "otherEntityName": "post", + "relationshipType": "many-to-one", + "relationshipValidateRules": [ + "required" + ], + "otherEntityField": "title" + } + ], + "fields": [ + { + "fieldName": "text", + "fieldType": "String", + "fieldValidateRules": [ + "required", + "minlength", + "maxlength" + ], + "fieldValidateRulesMinlength": "10", + "fieldValidateRulesMaxlength": "100" + }, + { + "fieldName": "creationDate", + "fieldType": "LocalDate", + "fieldValidateRules": [ + "required" + ] + } + ], + "changelogDate": "20170316224021", + "dto": "no", + "service": "no", + "entityTableName": "comment", + "pagination": "infinite-scroll" +} diff --git a/jhipster/.jhipster/Post.json b/jhipster/.jhipster/Post.json new file mode 100644 index 0000000000..595cf43598 --- /dev/null +++ b/jhipster/.jhipster/Post.json @@ -0,0 +1,52 @@ +{ + "fluentMethods": true, + "relationships": [ + { + "relationshipName": "creator", + "otherEntityName": "user", + "relationshipType": "many-to-one", + "relationshipValidateRules": [ + "required" + ], + "otherEntityField": "login", + "ownerSide": true, + "otherEntityRelationshipName": "post" + } + ], + "fields": [ + { + "fieldName": "title", + "fieldType": "String", + "fieldValidateRules": [ + "required", + "minlength", + "maxlength" + ], + "fieldValidateRulesMinlength": "10", + "fieldValidateRulesMaxlength": "100" + }, + { + "fieldName": "content", + "fieldType": "String", + "fieldValidateRules": [ + "required", + "minlength", + "maxlength" + ], + "fieldValidateRulesMinlength": "10", + "fieldValidateRulesMaxlength": "1000" + }, + { + "fieldName": "creationDate", + "fieldType": "LocalDate", + "fieldValidateRules": [ + "required" + ] + } + ], + "changelogDate": "20170316223211", + "dto": "no", + "service": "no", + "entityTableName": "post", + "pagination": "infinite-scroll" +} diff --git a/jhipster/.mvn/wrapper/maven-wrapper.jar b/jhipster/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5fd4d5023f1463b5ba3970e33c460c1eb26d748d GIT binary patch literal 49502 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%0A$pwhjXzyA0ZzErH{1@M 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 OJOEd-=rxTO#Qy`$*1Hh^ literal 0 HcmV?d00001 diff --git a/jhipster/.mvn/wrapper/maven-wrapper.properties b/jhipster/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..c954cec91c --- /dev/null +++ b/jhipster/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip diff --git a/jhipster/.travis.yml b/jhipster/.travis.yml new file mode 100644 index 0000000000..c34d1a7da6 --- /dev/null +++ b/jhipster/.travis.yml @@ -0,0 +1,41 @@ +os: + - linux +services: + - docker +language: node_js +node_js: + - "6.10.0" +jdk: + - oraclejdk8 +sudo: false +cache: + directories: + - node + - node_modules + - $HOME/.m2 +env: + global: + - NODE_VERSION=6.10.0 + - SPRING_OUTPUT_ANSI_ENABLED=ALWAYS + - SPRING_JPA_SHOW_SQL=false +before_install: + - jdk_switcher use oraclejdk8 + - java -version + - sudo /etc/init.d/mysql stop + - sudo /etc/init.d/postgresql stop + - nvm install $NODE_VERSION + - npm install -g npm + - node -v + - npm -v +install: + - npm install +script: + - chmod +x mvnw + - ./mvnw clean test + - npm test + - ./mvnw package -Pprod -DskipTests +notifications: + webhooks: + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: false # default: false diff --git a/jhipster/.yo-rc.json b/jhipster/.yo-rc.json new file mode 100644 index 0000000000..155e33b1c5 --- /dev/null +++ b/jhipster/.yo-rc.json @@ -0,0 +1,36 @@ +{ + "generator-jhipster": { + "jhipsterVersion": "4.0.8", + "baseName": "baeldung", + "packageName": "com.baeldung", + "packageFolder": "com/baeldung", + "serverPort": "8080", + "authenticationType": "jwt", + "hibernateCache": "ehcache", + "clusteredHttpSession": false, + "websocket": false, + "databaseType": "sql", + "devDatabaseType": "h2Disk", + "prodDatabaseType": "mysql", + "searchEngine": false, + "messageBroker": false, + "serviceDiscoveryType": false, + "buildTool": "maven", + "enableSocialSignIn": false, + "jwtSecretKey": "e1d4b69d3f953e3fa622121e882e6f459ca20ca4", + "clientFramework": "angular2", + "useSass": true, + "clientPackageManager": "npm", + "applicationType": "monolith", + "testFrameworks": [ + "gatling", + "protractor" + ], + "jhiPrefix": "jhi", + "enableTranslation": true, + "nativeLanguage": "en", + "languages": [ + "en" + ] + } +} \ No newline at end of file diff --git a/jhipster/Jenkinsfile b/jhipster/Jenkinsfile new file mode 100644 index 0000000000..1f0873a472 --- /dev/null +++ b/jhipster/Jenkinsfile @@ -0,0 +1,50 @@ +#!/usr/bin/env groovy + +node { + stage('checkout') { + checkout scm + } + + stage('check java') { + sh "java -version" + } + + stage('clean') { + sh "chmod +x mvnw" + sh "./mvnw clean" + } + + stage('install tools') { + sh "./mvnw com.github.eirslett:frontend-maven-plugin:install-node-and-npm -DnodeVersion=v6.10.0 -DnpmVersion=4.3.0" + } + + stage('npm install') { + sh "./mvnw com.github.eirslett:frontend-maven-plugin:npm" + } + + stage('backend tests') { + try { + sh "./mvnw test" + } catch(err) { + throw err + } finally { + junit '**/target/surefire-reports/TEST-*.xml' + } + } + + stage('frontend tests') { + try { + sh "./mvnw com.github.eirslett:frontend-maven-plugin:npm -Dfrontend.yarn.arguments=test" + } catch(err) { + throw err + } finally { + junit '**/target/test-results/karma/TESTS-*.xml' + } + } + + stage('packaging') { + sh "./mvnw package -Pprod -DskipTests" + archiveArtifacts artifacts: '**/target/*.war', fingerprint: true + } + +} diff --git a/jhipster/README.md b/jhipster/README.md new file mode 100644 index 0000000000..d45796d867 --- /dev/null +++ b/jhipster/README.md @@ -0,0 +1,153 @@ +# baeldung +This application was generated using JHipster 4.0.8, you can find documentation and help at [https://jhipster.github.io/documentation-archive/v4.0.8](https://jhipster.github.io/documentation-archive/v4.0.8). + +## Development + +Before you can build this project, you must install and configure the following dependencies on your machine: + +1. [Node.js][]: We use Node to run a development web server and build the project. + Depending on your system, you can install Node either from source or as a pre-packaged bundle. + +After installing Node, you should be able to run the following command to install development tools. +You will only need to run this command when dependencies change in [package.json](package.json). + + npm install + +We use npm scripts and [Webpack][] as our build system. + + +Run the following commands in two separate terminals to create a blissful development experience where your browser +auto-refreshes when files change on your hard drive. + + ./mvnw + npm start + +[Npm][] is also used to manage CSS and JavaScript dependencies used in this application. You can upgrade dependencies by +specifying a newer version in [package.json](package.json). You can also run `npm update` and `npm install` to manage dependencies. +Add the `help` flag on any command to see how you can use it. For example, `npm help update`. + +The `npm run` command will list all of the scripts available to run for this project. + +### Managing dependencies + +For example, to add [Leaflet][] library as a runtime dependency of your application, you would run following command: + + npm install --save --save-exact leaflet + +To benefit from TypeScript type definitions from [DefinitelyTyped][] repository in development, you would run following command: + + npm install --save-dev --save-exact @types/leaflet + +Then you would import the JS and CSS files specified in library's installation instructions so that [Webpack][] knows about them: + +Edit [src/main/webapp/app/vendor.ts](src/main/webapp/app/vendor.ts) file: +~~~ +import 'leaflet/dist/leaflet.js'; +~~~ + +Edit [src/main/webapp/content/css/vendor.css](src/main/webapp/content/css/vendor.css) file: +~~~ +@import '~leaflet/dist/leaflet.css'; +~~~ + +Note: there are still few other things remaining to do for Leaflet that we won't detail here. + +For further instructions on how to develop with JHipster, have a look at [Using JHipster in development][]. + +### Using angular-cli + +You can also use [Angular CLI][] to generate some custom client code. + +For example, the following command: + + ng generate component my-component + +will generate few files: + + create src/main/webapp/app/my-component/my-component.component.html + create src/main/webapp/app/my-component/my-component.component.ts + update src/main/webapp/app/app.module.ts + +## Building for production + +To optimize the baeldung application for production, run: + + ./mvnw -Pprod clean package + +This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references these new files. +To ensure everything worked, run: + + java -jar target/*.war + +Then navigate to [http://localhost:8080](http://localhost:8080) in your browser. + +Refer to [Using JHipster in production][] for more details. + +## Testing + +To launch your application's tests, run: + + ./mvnw clean test + +### Client tests + +Unit tests are run by [Karma][] and written with [Jasmine][]. They're located in [src/test/javascript/](src/test/javascript/) and can be run with: + + npm test + +UI end-to-end tests are powered by [Protractor][], which is built on top of WebDriverJS. They're located in [src/test/javascript/e2e](src/test/javascript/e2e) +and can be run by starting Spring Boot in one terminal (`./mvnw spring-boot:run`) and running the tests (`gulp itest`) in a second one. +### Other tests + +Performance tests are run by [Gatling][] and written in Scala. They're located in [src/test/gatling](src/test/gatling) and can be run with: + + ./mvnw gatling:execute + +For more information, refer to the [Running tests page][]. + +## Using Docker to simplify development (optional) + +You can use Docker to improve your JHipster development experience. A number of docker-compose configuration are available in the [src/main/docker](src/main/docker) folder to launch required third party services. +For example, to start a mysql database in a docker container, run: + + docker-compose -f src/main/docker/mysql.yml up -d + +To stop it and remove the container, run: + + docker-compose -f src/main/docker/mysql.yml down + +You can also fully dockerize your application and all the services that it depends on. +To achieve this, first build a docker image of your app by running: + + ./mvnw package -Pprod docker:build + +Then run: + + docker-compose -f src/main/docker/app.yml up -d + +For more information refer to [Using Docker and Docker-Compose][], this page also contains information on the docker-compose sub-generator (`yo jhipster:docker-compose`), which is able to generate docker configurations for one or several JHipster applications. + +## Continuous Integration (optional) + +To configure CI for your project, run the ci-cd sub-generator (`yo jhipster:ci-cd`), this will let you generate configuration files for a number of Continuous Integration systems. Consult the [Setting up Continuous Integration][] page for more information. + +[JHipster Homepage and latest documentation]: https://jhipster.github.io +[JHipster 4.0.8 archive]: https://jhipster.github.io/documentation-archive/v4.0.8 + +[Using JHipster in development]: https://jhipster.github.io/documentation-archive/v4.0.8/development/ +[Using Docker and Docker-Compose]: https://jhipster.github.io/documentation-archive/v4.0.8/docker-compose +[Using JHipster in production]: https://jhipster.github.io/documentation-archive/v4.0.8/production/ +[Running tests page]: https://jhipster.github.io/documentation-archive/v4.0.8/running-tests/ +[Setting up Continuous Integration]: https://jhipster.github.io/documentation-archive/v4.0.8/setting-up-ci/ + +[Gatling]: http://gatling.io/ +[Node.js]: https://nodejs.org/ +[Yarn]: https://yarnpkg.org/ +[Webpack]: https://webpack.github.io/ +[Angular CLI]: https://cli.angular.io/ +[BrowserSync]: http://www.browsersync.io/ +[Karma]: http://karma-runner.github.io/ +[Jasmine]: http://jasmine.github.io/2.0/introduction.html +[Protractor]: https://angular.github.io/protractor/ +[Leaflet]: http://leafletjs.com/ +[DefinitelyTyped]: http://definitelytyped.org/ diff --git a/jhipster/angular-cli.json b/jhipster/angular-cli.json new file mode 100644 index 0000000000..15558a2fae --- /dev/null +++ b/jhipster/angular-cli.json @@ -0,0 +1,55 @@ +{ + "project": { + "version": "1.0.0-beta.24", + "name": "baeldung" + }, + "apps": [ + { + "root": "src/main/webapp/", + "outDir": "target/www/app", + "assets": [ + "content", + "favicon.ico" + ], + "index": "index.html", + "main": "app/app.main.ts", + "test": "", + "tsconfig": "../../../tsconfig.json", + "prefix": "jhi", + "mobile": false, + "styles": [ + "content/css/main.css" + ], + "scripts": [], + "environments": {} + } + ], + "addons": [], + "packages": [], + "e2e": { + "protractor": { + "config": "src/test/javascript/protractor.conf.js" + } + }, + "test": { + "karma": { + "config": "src/test/javascript/karma.conf.js" + } + }, + "defaults": { + "styleExt": "css", + "prefixInterfaces": false, + "inline": { + "style": true, + "template": false + }, + "spec": { + "class": false, + "component": false, + "directive": false, + "module": false, + "pipe": false, + "service": false + } + } +} diff --git a/jhipster/circle.yml b/jhipster/circle.yml new file mode 100644 index 0000000000..bc9371e94f --- /dev/null +++ b/jhipster/circle.yml @@ -0,0 +1,25 @@ +machine: + services: + - docker + java: + version: oraclejdk8 + node: + version: 6.10.0 +dependencies: + cache_directories: + - node + - node_modules + - ~/.m2 + override: + - java -version + - npm install -g npm + - node -v + - npm -v + - java -version + - npm install +test: + override: + - chmod +x mvnw + - ./mvnw clean test + - npm test + - ./mvnw package -Pprod -DskipTests diff --git a/jhipster/mvnw b/jhipster/mvnw new file mode 100755 index 0000000000..a1ba1bf554 --- /dev/null +++ b/jhipster/mvnw @@ -0,0 +1,233 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + 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 + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} "$@" diff --git a/jhipster/mvnw.cmd b/jhipster/mvnw.cmd new file mode 100644 index 0000000000..2b934e89dd --- /dev/null +++ b/jhipster/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% \ No newline at end of file diff --git a/jhipster/package.json b/jhipster/package.json new file mode 100644 index 0000000000..40bbd28799 --- /dev/null +++ b/jhipster/package.json @@ -0,0 +1,112 @@ +{ + "name": "baeldung", + "version": "0.0.0", + "description": "Description for baeldung", + "private": true, + "cacheDirectories": [ + "node_modules" + ], + "dependencies": { + "@angular/common": "2.4.7", + "@angular/compiler": "2.4.7", + "@angular/core": "2.4.7", + "@angular/forms": "2.4.7", + "@angular/http": "2.4.7", + "@angular/platform-browser": "2.4.7", + "@angular/platform-browser-dynamic": "2.4.7", + "@angular/router": "3.4.7", + "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.20", + "angular2-infinite-scroll": "0.3.0", + "bootstrap": "4.0.0-alpha.6", + "font-awesome": "4.7.0", + "angular2-cookie": "1.2.6", + "core-js": "2.4.1", + "jquery": "3.1.1", + "ng-jhipster": "0.1.9", + "ng2-webstorage": "1.5.0", + "reflect-metadata": "0.1.9", + "rxjs": "5.1.0", + "swagger-ui": "2.2.10", + "tether": "1.4.0", + "zone.js": "0.7.6" + }, + "devDependencies": { + "@angular/cli": "1.0.0-beta.28.3", + "@types/jasmine": "2.5.42", + "@types/node": "7.0.5", + "@types/selenium-webdriver": "2.53.39", + "add-asset-html-webpack-plugin": "1.0.2", + "angular2-template-loader": "0.6.2", + "awesome-typescript-loader": "3.0.7", + "browser-sync": "2.18.7", + "browser-sync-webpack-plugin": "1.1.4", + "codelyzer": "2.0.0", + "copy-webpack-plugin": "4.0.1", + "css-loader": "0.26.1", + "del": "2.2.2", + "event-stream": "3.3.4", + "exports-loader": "0.6.3", + "extract-text-webpack-plugin": "2.0.0-beta.5", + "file-loader": "0.10.0", + "generator-jhipster": "4.0.8", + "html-webpack-plugin": "2.28.0", + "image-webpack-loader": "3.2.0", + "jasmine-core": "2.5.2", + "jasmine-reporters": "2.2.0", + "karma": "1.4.1", + "karma-chrome-launcher": "2.0.0", + "karma-coverage": "1.1.1", + "karma-intl-shim": "1.0.3", + "karma-jasmine": "1.1.0", + "karma-junit-reporter": "1.2.0", + "karma-phantomjs-launcher": "1.0.2", + "karma-remap-istanbul": "0.6.0", + "karma-sourcemap-loader": "0.3.7", + "karma-webpack": "2.0.2", + "lazypipe": "1.0.1", + "lodash": "4.17.4", + "map-stream": "0.0.6", + "phantomjs-prebuilt": "2.1.14", + "protractor": "5.1.1", + "protractor-jasmine2-screenshot-reporter": "0.3.3", + "ts-node": "2.1.0", + "proxy-middleware": "0.15.0", + "raw-loader": "0.5.1", + "run-sequence": "1.2.2", + "sourcemap-istanbul-instrumenter-loader": "0.2.0", + "string-replace-webpack-plugin": "0.0.5", + "style-loader": "0.13.1", + "to-string-loader": "1.1.5", + "tslint": "4.4.2", + "tslint-loader": "3.4.1", + "typescript": "2.1.6", + "webpack": "2.2.1", + "webpack-dev-server": "2.3.0", + "webpack-merge": "2.6.1", + "webpack-visualizer-plugin": "0.1.10", + "write-file-webpack-plugin": "3.4.2", + "xml2js": "0.4.17", + "sass-loader": "5.0.1", + "node-sass": "4.5.0", + "postcss-loader": "1.3.0", + "yargs": "6.6.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "scripts": { + "lint": "tslint 'src/main/webapp/app/**/*.ts' --force", + "lint:fix": "tslint 'src/main/webapp/app/**/*.ts' --fix --force", + "tsc": "tsc", + "tsc:w": "tsc -w", + "start": "npm run webpack:dev", + "webpack:build": "webpack --config webpack/webpack.vendor.js && webpack --config webpack/webpack.dev.js", + "webpack:build:dev": "webpack --config webpack/webpack.dev.js", + "webpack:dev": "webpack-dev-server --config webpack/webpack.dev.js --progress --inline --hot --profile --port=9060", + "webpack:prod": "npm test && webpack -p --config webpack/webpack.vendor.js && webpack -p --config webpack/webpack.prod.js", + "test": "npm run lint && karma start src/test/javascript/karma.conf.js", + "test:watch": "karma start --watch", + "e2e": "protractor src/test/javascript/protractor.conf.js", + "postinstall": "webdriver-manager update && npm run webpack:build" + } +} diff --git a/jhipster/pom.xml b/jhipster/pom.xml new file mode 100644 index 0000000000..ba78ad4e2b --- /dev/null +++ b/jhipster/pom.xml @@ -0,0 +1,1034 @@ + + + 4.0.0 + + + spring-boot-starter-parent + org.springframework.boot + 1.5.2.RELEASE + + + + com.baeldung + jhipster-monolithic + 0.0.1-SNAPSHOT + war + JHipster Monolithic Application + + + ${maven.version} + + + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.h2database + h2 + + + com.jayway.jsonpath + json-path + test + + + + com.jcraft + jzlib + ${jzlib.version} + + + com.mattbertolini + liquibase-slf4j + ${liquibase-slf4j.version} + + + com.ryantenney.metrics + metrics-spring + ${metrics-spring.version} + + + metrics-annotation + com.codahale.metrics + + + metrics-core + com.codahale.metrics + + + metrics-healthchecks + com.codahale.metrics + + + + + com.zaxxer + HikariCP + + + tools + com.sun + + + + + + commons-io + commons-io + ${commons-io.version} + + + io.dropwizard.metrics + metrics-annotation + ${dropwizard-metrics.version} + + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-json + ${dropwizard-metrics.version} + + + io.dropwizard.metrics + metrics-jvm + ${dropwizard-metrics.version} + + + io.dropwizard.metrics + metrics-servlet + ${dropwizard-metrics.version} + + + io.dropwizard.metrics + metrics-servlets + + + io.gatling.highcharts + gatling-charts-highcharts + ${gatling.version} + test + + + io.github.jhipster + jhipster + ${jhipster.server.version} + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + io.springfox + springfox-bean-validators + ${springfox.version} + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + mapstruct + org.mapstruct + + + + + javax.cache + cache-api + + + mysql + mysql-connector-java + + + + net.logstash.logback + logstash-logback-encoder + ${logstash-logback-encoder.version} + + + logback-core + ch.qos.logback + + + logback-classic + ch.qos.logback + + + logback-access + ch.qos.logback + + + + + org.apache.commons + commons-lang3 + ${commons-lang.version} + + + org.assertj + assertj-core + test + + + org.awaitility + awaitility + ${awaitility.version} + test + + + org.ehcache + ehcache + + + org.hibernate + hibernate-envers + + + org.hibernate + hibernate-jcache + ${hibernate.version} + + + org.hibernate + hibernate-validator + + + org.liquibase + liquibase-core + + + jetty-servlet + org.eclipse.jetty + + + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + org.springframework + spring-context-support + + + org.springframework.boot + spring-boot-actuator + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-loader-tools + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-cloud-connectors + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + spring-boot-starter-tomcat + org.springframework.boot + + + + + org.springframework.boot + spring-boot-test + test + + + + org.springframework.security + spring-security-data + + + org.springframework.security + spring-security-test + test + + + + + spring-boot:run + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + prepare-agent + + + + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + install-node-and-npm + npm + + + + + + + + + + + + + + + com.github.ekryd.sortpom + sortpom-maven-plugin + ${sortpom-maven-plugin.version} + + + verify + + sort + + + + + true + 4 + groupId,artifactId + groupId,artifactId + true + false + + + + com.spotify + docker-maven-plugin + ${docker-maven-plugin.version} + + baeldung + src/main/docker + + + / + ${project.build.directory} + ${project.build.finalName}.war + + + + + + io.gatling + gatling-maven-plugin + ${gatling-maven-plugin.version} + + src/test/gatling/conf + src/test/gatling/data + target/gatling/results + src/test/gatling/bodies + src/test/gatling/simulations + + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 1.8 + 1.8 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + + true + true + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-versions + + enforce + + + + + + + You are running an older version of + Maven. JHipster requires at least Maven + ${maven.version} + [${maven.version},) + + + You are running an older version of + Java. JHipster requires at least JDK + ${java.version} + [${java.version}.0,) + + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + default-resources + validate + + copy-resources + + + target/classes + false + + # + + + + src/main/resources/ + true + + **/*.xml + **/*.yml + + + + src/main/resources/ + false + + **/*.xml + **/*.yml + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + alphabetical + + **/*IntTest.java + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + pre-unit-tests + + prepare-agent + + + + ${project.testresult.directory}/coverage/jacoco/jacoco.exec + + + + + post-unit-test + test + + report + + + ${project.testresult.directory}/coverage/jacoco/jacoco.exec + ${project.testresult.directory}/coverage/jacoco + + + + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + + + javax.validation + validation-api + ${validation-api.version} + + + org.javassist + javassist + ${javassist.version} + + + org.liquibase.ext + liquibase-hibernate5 + ${liquibase-hibernate5.version} + + + org.springframework.boot + spring-boot-starter-data-jpa + ${project.parent.version} + + + + src/main/resources/config/liquibase/master.xml + src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml + org.h2.Driver + jdbc:h2:file:./target/h2db/db/baeldung + + baeldung + + hibernate:spring:com.baeldung.domain?dialect=org.hibernate.dialect.H2Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + true + debug + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + org.springframework.boot + spring-boot-maven-plugin + + true + true + + + + + + + + + no-liquibase + + ,no-liquibase + + + + swagger + + ,swagger + + + + webpack + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + + install node and npm + + install-node-and-npm + + + ${node.version} + ${npm.version} + + + + webpack build dev + generate-resources + + npm + + + run webpack:build:dev + + + + + + + + + dev + + true + + + + + org.apache.maven.plugins + maven-war-plugin + + src/main/webapp/ + + + + + + + DEBUG + + dev${profile.no-liquibase} + + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-undertow + + + + + prod + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + + install node and npm + + install-node-and-npm + + + ${node.version} + ${npm.version} + + + + npm install + + npm + + + install + + + + npm rebuild node-sass + + npm + + + rebuild node-sass + + + + webpack build prod + generate-resources + + npm + + + run webpack:prod + + + + + + maven-clean-plugin + + + + target/www/ + + + + + + org.apache.maven.plugins + maven-war-plugin + + target/www/ + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + true + + + + + + + INFO + + prod${profile.swagger}${profile.no-liquibase} + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + cc + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin.version} + + + compile + compile + + add-source + compile + + + + test-compile + test-compile + + add-source + testCompile + + + + + incremental + true + ${scala.version} + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + + + org.apache.maven.plugins + maven-war-plugin + + src/main/webapp/ + + + + org.springframework.boot + spring-boot-maven-plugin + + true + true + true + + + + + + + + DEBUG + + dev,swagger + + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + graphite + + + io.dropwizard.metrics + metrics-graphite + + + + + + prometheus + + + io.prometheus + simpleclient + ${prometheus-simpleclient.version} + + + io.prometheus + simpleclient_dropwizard + ${prometheus-simpleclient.version} + + + io.prometheus + simpleclient_servlet + ${prometheus-simpleclient.version} + + + + + + IDE + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + integration + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration-test + + test + + + + **/*IntTest.java + + + + + + + + + + + + -Djava.security.egd=file:/dev/./urandom -Xmx256m + 3.6.2 + 2.0.0 + 2.5 + 3.5 + 0.4.13 + 1.3 + 2.2.1 + 2.2.3 + 5.2.8.Final + 2.6.0 + 0.7.9 + 1.8 + 3.21.0-GA + 1.0.0 + 1.1.0 + 0.7.0 + 1.1.3 + 3.6 + 2.0.0 + 4.8 + jdt_apt + 1.1.0.Final + 3.6.0 + 1.4.1 + 3.0.1 + yyyyMMddHHmmss + ${java.version} + ${java.version} + 3.0.0 + 3.1.3 + v6.10.0 + 4.3.0 + + + + + ${project.build.directory}/test-results + 0.0.20 + false + 3.2.2 + 2.12.1 + 3.2 + + src/main/webapp/content/**/*.*, + src/main/webapp/bower_components/**/*.*, + src/main/webapp/i18n/*.js, target/www/**/*.* + + S3437,UndocumentedApi,BoldAndItalicTagsCheck + + + src/main/webapp/app/**/*.* + Web:BoldAndItalicTagsCheck + + src/main/java/**/* + squid:S3437 + + src/main/java/**/* + squid:UndocumentedApi + + ${project.testresult.directory}/coverage/jacoco/jacoco-it.exec + ${project.testresult.directory}/coverage/jacoco/jacoco.exec + jacoco + + ${project.testresult.directory}/karma + + ${project.testresult.directory}/coverage/report-lcov/lcov.info + + ${project.testresult.directory}/coverage/report-lcov/lcov.info + + ${project.basedir}/src/main/ + ${project.testresult.directory}/surefire-reports + ${project.basedir}/src/test/ + + 2.5.0 + + 2.6.1 + 1.4.10.Final + 1.1.0.Final + + diff --git a/jhipster/postcss.config.js b/jhipster/postcss.config.js new file mode 100644 index 0000000000..f549c034d5 --- /dev/null +++ b/jhipster/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [] +} diff --git a/jhipster/src/main/docker/Dockerfile b/jhipster/src/main/docker/Dockerfile new file mode 100644 index 0000000000..66991b2998 --- /dev/null +++ b/jhipster/src/main/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM openjdk:8-jre-alpine + +ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \ + JHIPSTER_SLEEP=0 + +# add directly the war +ADD *.war /app.war + +VOLUME /tmp +EXPOSE 8080 +CMD echo "The application will start in ${JHIPSTER_SLEEP}s..." && \ + sleep ${JHIPSTER_SLEEP} && \ + java -Djava.security.egd=file:/dev/./urandom -jar /app.war diff --git a/jhipster/src/main/docker/app.yml b/jhipster/src/main/docker/app.yml new file mode 100644 index 0000000000..78cd2c1602 --- /dev/null +++ b/jhipster/src/main/docker/app.yml @@ -0,0 +1,14 @@ +version: '2' +services: + baeldung-app: + image: baeldung + environment: + - SPRING_PROFILES_ACTIVE=prod,swagger + - SPRING_DATASOURCE_URL=jdbc:mysql://baeldung-mysql:3306/baeldung?useUnicode=true&characterEncoding=utf8&useSSL=false + - JHIPSTER_SLEEP=10 # gives time for the database to boot before the application + ports: + - 8080:8080 + baeldung-mysql: + extends: + file: mysql.yml + service: baeldung-mysql diff --git a/jhipster/src/main/docker/mysql.yml b/jhipster/src/main/docker/mysql.yml new file mode 100644 index 0000000000..5038d09504 --- /dev/null +++ b/jhipster/src/main/docker/mysql.yml @@ -0,0 +1,13 @@ +version: '2' +services: + baeldung-mysql: + image: mysql:5.7.13 + # volumes: + # - ~/volumes/jhipster/baeldung/mysql/:/var/lib/mysql/ + environment: + - MYSQL_USER=root + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=baeldung + ports: + - 3306:3306 + command: mysqld --lower_case_table_names=1 --skip-ssl --character_set_server=utf8 diff --git a/jhipster/src/main/docker/sonar.yml b/jhipster/src/main/docker/sonar.yml new file mode 100644 index 0000000000..718be83b4d --- /dev/null +++ b/jhipster/src/main/docker/sonar.yml @@ -0,0 +1,7 @@ +version: '2' +services: + baeldung-sonar: + image: sonarqube:6.2-alpine + ports: + - 9000:9000 + - 9092:9092 diff --git a/jhipster/src/main/java/com/baeldung/ApplicationWebXml.java b/jhipster/src/main/java/com/baeldung/ApplicationWebXml.java new file mode 100644 index 0000000000..762d18420c --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/ApplicationWebXml.java @@ -0,0 +1,21 @@ +package com.baeldung; + +import com.baeldung.config.DefaultProfileUtil; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.support.SpringBootServletInitializer; + +/** + * This is a helper Java class that provides an alternative to creating a web.xml. + * This will be invoked only when the application is deployed to a servlet container like Tomcat, JBoss etc. + */ +public class ApplicationWebXml extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + /** + * set a default to use when no profile is configured. + */ + DefaultProfileUtil.addDefaultProfile(application.application()); + return application.sources(BaeldungApp.class); + } +} diff --git a/jhipster/src/main/java/com/baeldung/BaeldungApp.java b/jhipster/src/main/java/com/baeldung/BaeldungApp.java new file mode 100644 index 0000000000..13b6b2b3fa --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/BaeldungApp.java @@ -0,0 +1,84 @@ +package com.baeldung; + +import com.baeldung.config.ApplicationProperties; +import com.baeldung.config.DefaultProfileUtil; + +import io.github.jhipster.config.JHipsterConstants; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.*; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.env.Environment; + +import javax.annotation.PostConstruct; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; + +@ComponentScan +@EnableAutoConfiguration(exclude = {MetricFilterAutoConfiguration.class, MetricRepositoryAutoConfiguration.class}) +@EnableConfigurationProperties({LiquibaseProperties.class, ApplicationProperties.class}) +public class BaeldungApp { + + private static final Logger log = LoggerFactory.getLogger(BaeldungApp.class); + + private final Environment env; + + public BaeldungApp(Environment env) { + this.env = env; + } + + /** + * Initializes baeldung. + *

+ * Spring profiles can be configured with a program arguments --spring.profiles.active=your-active-profile + *

+ * You can find more information on how profiles work with JHipster on http://jhipster.github.io/profiles/. + */ + @PostConstruct + public void initApplication() { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + log.error("You have misconfigured your application! It should not run " + + "with both the 'dev' and 'prod' profiles at the same time."); + } + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_CLOUD)) { + log.error("You have misconfigured your application! It should not" + + "run with both the 'dev' and 'cloud' profiles at the same time."); + } + } + + /** + * Main method, used to run the application. + * + * @param args the command line arguments + * @throws UnknownHostException if the local host name could not be resolved into an address + */ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplication(BaeldungApp.class); + DefaultProfileUtil.addDefaultProfile(app); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("\n----------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}\n----------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/jhipster/src/main/java/com/baeldung/aop/logging/LoggingAspect.java b/jhipster/src/main/java/com/baeldung/aop/logging/LoggingAspect.java new file mode 100644 index 0000000000..7fbd352820 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/aop/logging/LoggingAspect.java @@ -0,0 +1,79 @@ +package com.baeldung.aop.logging; + +import io.github.jhipster.config.JHipsterConstants; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +import java.util.Arrays; + +/** + * Aspect for logging execution of service and repository Spring components. + * + * By default, it only runs with the "dev" profile. + */ +@Aspect +public class LoggingAspect { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final Environment env; + + public LoggingAspect(Environment env) { + this.env = env; + } + + /** + * Pointcut that matches all repositories, services and Web REST endpoints. + */ + @Pointcut("within(com.baeldung.repository..*) || within(com.baeldung.service..*) || within(com.baeldung.web.rest..*)") + public void loggingPointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Advice that logs methods throwing exceptions. + */ + @AfterThrowing(pointcut = "loggingPointcut()", throwing = "e") + public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { + if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)) { + log.error("Exception in {}.{}() with cause = \'{}\' and exception = \'{}\'", joinPoint.getSignature().getDeclaringTypeName(), + joinPoint.getSignature().getName(), e.getCause() != null? e.getCause() : "NULL", e.getMessage(), e); + + } else { + log.error("Exception in {}.{}() with cause = {}", joinPoint.getSignature().getDeclaringTypeName(), + joinPoint.getSignature().getName(), e.getCause() != null? e.getCause() : "NULL"); + } + } + + /** + * Advice that logs when a method is entered and exited. + */ + @Around("loggingPointcut()") + public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { + if (log.isDebugEnabled()) { + log.debug("Enter: {}.{}() with argument[s] = {}", joinPoint.getSignature().getDeclaringTypeName(), + joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); + } + try { + Object result = joinPoint.proceed(); + if (log.isDebugEnabled()) { + log.debug("Exit: {}.{}() with result = {}", joinPoint.getSignature().getDeclaringTypeName(), + joinPoint.getSignature().getName(), result); + } + return result; + } catch (IllegalArgumentException e) { + log.error("Illegal argument: {} in {}.{}()", Arrays.toString(joinPoint.getArgs()), + joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName()); + + throw e; + } + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/ApplicationProperties.java b/jhipster/src/main/java/com/baeldung/config/ApplicationProperties.java new file mode 100644 index 0000000000..add1782678 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/ApplicationProperties.java @@ -0,0 +1,15 @@ +package com.baeldung.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties specific to JHipster. + * + *

+ * Properties are configured in the application.yml file. + *

+ */ +@ConfigurationProperties(prefix = "application", ignoreUnknownFields = false) +public class ApplicationProperties { + +} diff --git a/jhipster/src/main/java/com/baeldung/config/AsyncConfiguration.java b/jhipster/src/main/java/com/baeldung/config/AsyncConfiguration.java new file mode 100644 index 0000000000..dc1ba37514 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/AsyncConfiguration.java @@ -0,0 +1,46 @@ +package com.baeldung.config; + +import io.github.jhipster.async.ExceptionHandlingAsyncTaskExecutor; +import io.github.jhipster.config.JHipsterProperties; + +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.*; +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); + + private final JHipsterProperties jHipsterProperties; + + public AsyncConfiguration(JHipsterProperties jHipsterProperties) { + this.jHipsterProperties = jHipsterProperties; + } + + @Override + @Bean(name = "taskExecutor") + public Executor getAsyncExecutor() { + log.debug("Creating Async Task Executor"); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(jHipsterProperties.getAsync().getCorePoolSize()); + executor.setMaxPoolSize(jHipsterProperties.getAsync().getMaxPoolSize()); + executor.setQueueCapacity(jHipsterProperties.getAsync().getQueueCapacity()); + executor.setThreadNamePrefix("baeldung-Executor-"); + return new ExceptionHandlingAsyncTaskExecutor(executor); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/CacheConfiguration.java b/jhipster/src/main/java/com/baeldung/config/CacheConfiguration.java new file mode 100644 index 0000000000..4323fa076a --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/CacheConfiguration.java @@ -0,0 +1,48 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterProperties; +import org.ehcache.config.builders.CacheConfigurationBuilder; +import org.ehcache.config.builders.ResourcePoolsBuilder; +import org.ehcache.expiry.Duration; +import org.ehcache.expiry.Expirations; +import org.ehcache.jsr107.Eh107Configuration; + +import java.util.concurrent.TimeUnit; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.*; + +@Configuration +@EnableCaching +@AutoConfigureAfter(value = { MetricsConfiguration.class }) +@AutoConfigureBefore(value = { WebConfigurer.class, DatabaseConfiguration.class }) +public class CacheConfiguration { + + private final javax.cache.configuration.Configuration jcacheConfiguration; + + public CacheConfiguration(JHipsterProperties jHipsterProperties) { + JHipsterProperties.Cache.Ehcache ehcache = + jHipsterProperties.getCache().getEhcache(); + + jcacheConfiguration = Eh107Configuration.fromEhcacheCacheConfiguration( + CacheConfigurationBuilder.newCacheConfigurationBuilder(Object.class, Object.class, + ResourcePoolsBuilder.heap(ehcache.getMaxEntries())) + .withExpiry(Expirations.timeToLiveExpiration(Duration.of(ehcache.getTimeToLiveSeconds(), TimeUnit.SECONDS))) + .build()); + } + + @Bean + public JCacheManagerCustomizer cacheManagerCustomizer() { + return cm -> { + cm.createCache(com.baeldung.domain.User.class.getName(), jcacheConfiguration); + cm.createCache(com.baeldung.domain.Authority.class.getName(), jcacheConfiguration); + cm.createCache(com.baeldung.domain.User.class.getName() + ".authorities", jcacheConfiguration); + cm.createCache(com.baeldung.domain.Post.class.getName(), jcacheConfiguration); + cm.createCache(com.baeldung.domain.Comment.class.getName(), jcacheConfiguration); + // jhipster-needle-ehcache-add-entry + }; + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/CloudDatabaseConfiguration.java b/jhipster/src/main/java/com/baeldung/config/CloudDatabaseConfiguration.java new file mode 100644 index 0000000000..cae9ec1e82 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/CloudDatabaseConfiguration.java @@ -0,0 +1,23 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterConstants; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.config.java.AbstractCloudConfig; +import org.springframework.context.annotation.*; + +import javax.sql.DataSource; + +@Configuration +@Profile(JHipsterConstants.SPRING_PROFILE_CLOUD) +public class CloudDatabaseConfiguration extends AbstractCloudConfig { + + private final Logger log = LoggerFactory.getLogger(CloudDatabaseConfiguration.class); + + @Bean + public DataSource dataSource() { + log.info("Configuring JDBC datasource from a cloud provider"); + return connectionFactory().dataSource(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/Constants.java b/jhipster/src/main/java/com/baeldung/config/Constants.java new file mode 100644 index 0000000000..9d797c1934 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/Constants.java @@ -0,0 +1,16 @@ +package com.baeldung.config; + +/** + * Application constants. + */ +public final class Constants { + + //Regex for acceptable logins + public static final String LOGIN_REGEX = "^[_'.@A-Za-z0-9-]*$"; + + public static final String SYSTEM_ACCOUNT = "system"; + public static final String ANONYMOUS_USER = "anonymoususer"; + + private Constants() { + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/DatabaseConfiguration.java b/jhipster/src/main/java/com/baeldung/config/DatabaseConfiguration.java new file mode 100644 index 0000000000..b64fd7cc82 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/DatabaseConfiguration.java @@ -0,0 +1,75 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterConstants; +import io.github.jhipster.config.liquibase.AsyncSpringLiquibase; + +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import liquibase.integration.spring.SpringLiquibase; +import org.h2.tools.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.sql.DataSource; +import java.sql.SQLException; + +@Configuration +@EnableJpaRepositories("com.baeldung.repository") +@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") +@EnableTransactionManagement +public class DatabaseConfiguration { + + private final Logger log = LoggerFactory.getLogger(DatabaseConfiguration.class); + + private final Environment env; + + public DatabaseConfiguration(Environment env) { + this.env = env; + } + + /** + * Open the TCP port for the H2 database, so it is available remotely. + * + * @return the H2 database TCP server + * @throws SQLException if the server failed to start + */ + @Bean(initMethod = "start", destroyMethod = "stop") + @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + public Server h2TCPServer() throws SQLException { + return Server.createTcpServer("-tcp","-tcpAllowOthers"); + } + + @Bean + public SpringLiquibase liquibase(@Qualifier("taskExecutor") TaskExecutor taskExecutor, + DataSource dataSource, LiquibaseProperties liquibaseProperties) { + + // Use liquibase.integration.spring.SpringLiquibase if you don't want Liquibase to start asynchronously + SpringLiquibase liquibase = new AsyncSpringLiquibase(taskExecutor, env); + liquibase.setDataSource(dataSource); + liquibase.setChangeLog("classpath:config/liquibase/master.xml"); + liquibase.setContexts(liquibaseProperties.getContexts()); + liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema()); + liquibase.setDropFirst(liquibaseProperties.isDropFirst()); + if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_NO_LIQUIBASE)) { + liquibase.setShouldRun(false); + } else { + liquibase.setShouldRun(liquibaseProperties.isEnabled()); + log.debug("Configuring Liquibase"); + } + return liquibase; + } + + @Bean + public Hibernate5Module hibernate5Module() { + return new Hibernate5Module(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/DateTimeFormatConfiguration.java b/jhipster/src/main/java/com/baeldung/config/DateTimeFormatConfiguration.java new file mode 100644 index 0000000000..aab57adfb0 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/DateTimeFormatConfiguration.java @@ -0,0 +1,17 @@ +package com.baeldung.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +@Configuration +public class DateTimeFormatConfiguration extends WebMvcConfigurerAdapter { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/DefaultProfileUtil.java b/jhipster/src/main/java/com/baeldung/config/DefaultProfileUtil.java new file mode 100644 index 0000000000..7918fb49a4 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/DefaultProfileUtil.java @@ -0,0 +1,48 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterConstants; + +import org.springframework.boot.SpringApplication; +import org.springframework.core.env.Environment; + +import java.util.*; + +/** + * Utility class to load a Spring profile to be used as default + * when there is no spring.profiles.active set in the environment or as command line argument. + * If the value is not available in application.yml then dev profile will be used as default. + */ +public final class DefaultProfileUtil { + + private static final String SPRING_PROFILE_DEFAULT = "spring.profiles.default"; + + private DefaultProfileUtil() { + } + + /** + * Set a default to use when no profile is configured. + * + * @param app the Spring application + */ + public static void addDefaultProfile(SpringApplication app) { + Map defProperties = new HashMap<>(); + /* + * The default profile to use when no other profiles are defined + * This cannot be set in the application.yml file. + * See https://github.com/spring-projects/spring-boot/issues/1219 + */ + defProperties.put(SPRING_PROFILE_DEFAULT, JHipsterConstants.SPRING_PROFILE_DEVELOPMENT); + app.setDefaultProperties(defProperties); + } + + /** + * Get the profiles that are applied else get default profiles. + */ + public static String[] getActiveProfiles(Environment env) { + String[] profiles = env.getActiveProfiles(); + if (profiles.length == 0) { + return env.getDefaultProfiles(); + } + return profiles; + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/LocaleConfiguration.java b/jhipster/src/main/java/com/baeldung/config/LocaleConfiguration.java new file mode 100644 index 0000000000..dd3a499d56 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/LocaleConfiguration.java @@ -0,0 +1,35 @@ +package com.baeldung.config; + +import io.github.jhipster.config.locale.AngularCookieLocaleResolver; + +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +@Configuration +public class LocaleConfiguration extends WebMvcConfigurerAdapter implements EnvironmentAware { + + @Override + public void setEnvironment(Environment environment) { + // unused + } + + @Bean(name = "localeResolver") + public LocaleResolver localeResolver() { + AngularCookieLocaleResolver cookieLocaleResolver = new AngularCookieLocaleResolver(); + cookieLocaleResolver.setCookieName("NG_TRANSLATE_LANG_KEY"); + return cookieLocaleResolver; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); + localeChangeInterceptor.setParamName("language"); + registry.addInterceptor(localeChangeInterceptor); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/LoggingAspectConfiguration.java b/jhipster/src/main/java/com/baeldung/config/LoggingAspectConfiguration.java new file mode 100644 index 0000000000..936f54709a --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/LoggingAspectConfiguration.java @@ -0,0 +1,19 @@ +package com.baeldung.config; + +import com.baeldung.aop.logging.LoggingAspect; + +import io.github.jhipster.config.JHipsterConstants; + +import org.springframework.context.annotation.*; +import org.springframework.core.env.Environment; + +@Configuration +@EnableAspectJAutoProxy +public class LoggingAspectConfiguration { + + @Bean + @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + public LoggingAspect loggingAspect(Environment env) { + return new LoggingAspect(env); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/LoggingConfiguration.java b/jhipster/src/main/java/com/baeldung/config/LoggingConfiguration.java new file mode 100644 index 0000000000..3ca6a48821 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/LoggingConfiguration.java @@ -0,0 +1,109 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterProperties; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggerContextListener; +import ch.qos.logback.core.spi.ContextAwareBase; +import net.logstash.logback.appender.LogstashSocketAppender; +import net.logstash.logback.stacktrace.ShortenedThrowableConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class LoggingConfiguration { + + private final Logger log = LoggerFactory.getLogger(LoggingConfiguration.class); + + private LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + + @Value("${spring.application.name}") + private String appName; + + @Value("${server.port}") + private String serverPort; + + private final JHipsterProperties jHipsterProperties; + + public LoggingConfiguration(JHipsterProperties jHipsterProperties) { + this.jHipsterProperties = jHipsterProperties; + if (jHipsterProperties.getLogging().getLogstash().isEnabled()) { + addLogstashAppender(context); + + // Add context listener + LogbackLoggerContextListener loggerContextListener = new LogbackLoggerContextListener(); + loggerContextListener.setContext(context); + context.addListener(loggerContextListener); + } + } + + public void addLogstashAppender(LoggerContext context) { + log.info("Initializing Logstash logging"); + + LogstashSocketAppender logstashAppender = new LogstashSocketAppender(); + logstashAppender.setName("LOGSTASH"); + logstashAppender.setContext(context); + String customFields = "{\"app_name\":\"" + appName + "\",\"app_port\":\"" + serverPort + "\"}"; + + // Set the Logstash appender config from JHipster properties + logstashAppender.setSyslogHost(jHipsterProperties.getLogging().getLogstash().getHost()); + logstashAppender.setPort(jHipsterProperties.getLogging().getLogstash().getPort()); + logstashAppender.setCustomFields(customFields); + + // Limit the maximum length of the forwarded stacktrace so that it won't exceed the 8KB UDP limit of logstash + ShortenedThrowableConverter throwableConverter = new ShortenedThrowableConverter(); + throwableConverter.setMaxLength(7500); + throwableConverter.setRootCauseFirst(true); + logstashAppender.setThrowableConverter(throwableConverter); + + logstashAppender.start(); + + // Wrap the appender in an Async appender for performance + AsyncAppender asyncLogstashAppender = new AsyncAppender(); + asyncLogstashAppender.setContext(context); + asyncLogstashAppender.setName("ASYNC_LOGSTASH"); + asyncLogstashAppender.setQueueSize(jHipsterProperties.getLogging().getLogstash().getQueueSize()); + asyncLogstashAppender.addAppender(logstashAppender); + asyncLogstashAppender.start(); + + context.getLogger("ROOT").addAppender(asyncLogstashAppender); + } + + /** + * Logback configuration is achieved by configuration file and API. + * When configuration file change is detected, the configuration is reset. + * This listener ensures that the programmatic configuration is also re-applied after reset. + */ + class LogbackLoggerContextListener extends ContextAwareBase implements LoggerContextListener { + + @Override + public boolean isResetResistant() { + return true; + } + + @Override + public void onStart(LoggerContext context) { + addLogstashAppender(context); + } + + @Override + public void onReset(LoggerContext context) { + addLogstashAppender(context); + } + + @Override + public void onStop(LoggerContext context) { + // Nothing to do. + } + + @Override + public void onLevelChange(ch.qos.logback.classic.Logger logger, Level level) { + // Nothing to do. + } + } + +} diff --git a/jhipster/src/main/java/com/baeldung/config/MetricsConfiguration.java b/jhipster/src/main/java/com/baeldung/config/MetricsConfiguration.java new file mode 100644 index 0000000000..94a50bed91 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/MetricsConfiguration.java @@ -0,0 +1,94 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterProperties; +import io.github.jhipster.config.jcache.JCacheGaugeSet; + +import com.codahale.metrics.JmxReporter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.codahale.metrics.jvm.*; +import com.ryantenney.metrics.spring.config.annotation.EnableMetrics; +import com.ryantenney.metrics.spring.config.annotation.MetricsConfigurerAdapter; +import com.zaxxer.hikari.HikariDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.*; + +import javax.annotation.PostConstruct; +import java.lang.management.ManagementFactory; +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableMetrics(proxyTargetClass = true) +public class MetricsConfiguration extends MetricsConfigurerAdapter { + + private static final String PROP_METRIC_REG_JVM_MEMORY = "jvm.memory"; + private static final String PROP_METRIC_REG_JVM_GARBAGE = "jvm.garbage"; + private static final String PROP_METRIC_REG_JVM_THREADS = "jvm.threads"; + private static final String PROP_METRIC_REG_JVM_FILES = "jvm.files"; + private static final String PROP_METRIC_REG_JVM_BUFFERS = "jvm.buffers"; + + private static final String PROP_METRIC_REG_JCACHE_STATISTICS = "jcache.statistics"; + private final Logger log = LoggerFactory.getLogger(MetricsConfiguration.class); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private HealthCheckRegistry healthCheckRegistry = new HealthCheckRegistry(); + + private final JHipsterProperties jHipsterProperties; + + private HikariDataSource hikariDataSource; + + public MetricsConfiguration(JHipsterProperties jHipsterProperties) { + this.jHipsterProperties = jHipsterProperties; + } + + @Autowired(required = false) + public void setHikariDataSource(HikariDataSource hikariDataSource) { + this.hikariDataSource = hikariDataSource; + } + + @Override + @Bean + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } + + @Override + @Bean + public HealthCheckRegistry getHealthCheckRegistry() { + return healthCheckRegistry; + } + + @PostConstruct + public void init() { + log.debug("Registering JVM gauges"); + metricRegistry.register(PROP_METRIC_REG_JVM_MEMORY, new MemoryUsageGaugeSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_GARBAGE, new GarbageCollectorMetricSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_THREADS, new ThreadStatesGaugeSet()); + metricRegistry.register(PROP_METRIC_REG_JVM_FILES, new FileDescriptorRatioGauge()); + metricRegistry.register(PROP_METRIC_REG_JVM_BUFFERS, new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer())); + + metricRegistry.register(PROP_METRIC_REG_JCACHE_STATISTICS, new JCacheGaugeSet()); + if (hikariDataSource != null) { + log.debug("Monitoring the datasource"); + hikariDataSource.setMetricRegistry(metricRegistry); + } + if (jHipsterProperties.getMetrics().getJmx().isEnabled()) { + log.debug("Initializing Metrics JMX reporting"); + JmxReporter jmxReporter = JmxReporter.forRegistry(metricRegistry).build(); + jmxReporter.start(); + } + if (jHipsterProperties.getMetrics().getLogs().isEnabled()) { + log.info("Initializing Metrics Log reporting"); + final Slf4jReporter reporter = Slf4jReporter.forRegistry(metricRegistry) + .outputTo(LoggerFactory.getLogger("metrics")) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + reporter.start(jHipsterProperties.getMetrics().getLogs().getReportFrequency(), TimeUnit.SECONDS); + } + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/SecurityConfiguration.java b/jhipster/src/main/java/com/baeldung/config/SecurityConfiguration.java new file mode 100644 index 0000000000..118d302fcf --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/SecurityConfiguration.java @@ -0,0 +1,127 @@ +package com.baeldung.config; + +import com.baeldung.security.*; +import com.baeldung.security.jwt.*; + +import io.github.jhipster.security.*; + +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.filter.CorsFilter; + +import javax.annotation.PostConstruct; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + private final UserDetailsService userDetailsService; + + private final TokenProvider tokenProvider; + + private final CorsFilter corsFilter; + + public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService, + TokenProvider tokenProvider, + CorsFilter corsFilter) { + + this.authenticationManagerBuilder = authenticationManagerBuilder; + this.userDetailsService = userDetailsService; + this.tokenProvider = tokenProvider; + this.corsFilter = corsFilter; + } + + @PostConstruct + public void init() { + try { + authenticationManagerBuilder + .userDetailsService(userDetailsService) + .passwordEncoder(passwordEncoder()); + } catch (Exception e) { + throw new BeanInitializationException("Security configuration failed", e); + } + } + + @Bean + public Http401UnauthorizedEntryPoint http401UnauthorizedEntryPoint() { + return new Http401UnauthorizedEntryPoint(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring() + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/app/**/*.{js,html}") + .antMatchers("/bower_components/**") + .antMatchers("/i18n/**") + .antMatchers("/content/**") + .antMatchers("/swagger-ui/index.html") + .antMatchers("/test/**") + .antMatchers("/h2-console/**"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling() + .authenticationEntryPoint(http401UnauthorizedEntryPoint()) + .and() + .csrf() + .disable() + .headers() + .frameOptions() + .disable() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/register").permitAll() + .antMatchers("/api/activate").permitAll() + .antMatchers("/api/authenticate").permitAll() + .antMatchers("/api/account/reset_password/init").permitAll() + .antMatchers("/api/account/reset_password/finish").permitAll() + .antMatchers("/api/profile-info").permitAll() + .antMatchers("/api/**").authenticated() + .antMatchers("/management/health").permitAll() + .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) + .antMatchers("/v2/api-docs/**").permitAll() + .antMatchers("/swagger-resources/configuration/ui").permitAll() + .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN) + .and() + .apply(securityConfigurerAdapter()); + + } + + private JWTConfigurer securityConfigurerAdapter() { + return new JWTConfigurer(tokenProvider); + } + + @Bean + public SecurityEvaluationContextExtension securityEvaluationContextExtension() { + return new SecurityEvaluationContextExtension(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/ThymeleafConfiguration.java b/jhipster/src/main/java/com/baeldung/config/ThymeleafConfiguration.java new file mode 100644 index 0000000000..68afcae71a --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/ThymeleafConfiguration.java @@ -0,0 +1,26 @@ +package com.baeldung.config; + +import org.apache.commons.lang3.CharEncoding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.*; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +@Configuration +public class ThymeleafConfiguration { + + @SuppressWarnings("unused") + private final Logger log = LoggerFactory.getLogger(ThymeleafConfiguration.class); + + @Bean + @Description("Thymeleaf template resolver serving HTML 5 emails") + public ClassLoaderTemplateResolver emailTemplateResolver() { + ClassLoaderTemplateResolver emailTemplateResolver = new ClassLoaderTemplateResolver(); + emailTemplateResolver.setPrefix("mails/"); + emailTemplateResolver.setSuffix(".html"); + emailTemplateResolver.setTemplateMode("HTML5"); + emailTemplateResolver.setCharacterEncoding(CharEncoding.UTF_8); + emailTemplateResolver.setOrder(1); + return emailTemplateResolver; + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/WebConfigurer.java b/jhipster/src/main/java/com/baeldung/config/WebConfigurer.java new file mode 100644 index 0000000000..bd80a063eb --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/WebConfigurer.java @@ -0,0 +1,186 @@ +package com.baeldung.config; + +import io.github.jhipster.config.JHipsterConstants; +import io.github.jhipster.config.JHipsterProperties; +import io.github.jhipster.web.filter.CachingHttpHeadersFilter; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.servlet.InstrumentedFilter; +import com.codahale.metrics.servlets.MetricsServlet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.*; +import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory; +import io.undertow.UndertowOptions; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.io.File; +import java.nio.file.Paths; +import java.util.*; +import javax.servlet.*; + +/** + * Configuration of web application with Servlet 3.0 APIs. + */ +@Configuration +public class WebConfigurer implements ServletContextInitializer, EmbeddedServletContainerCustomizer { + + private final Logger log = LoggerFactory.getLogger(WebConfigurer.class); + + private final Environment env; + + private final JHipsterProperties jHipsterProperties; + + private MetricRegistry metricRegistry; + + public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { + + this.env = env; + this.jHipsterProperties = jHipsterProperties; + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + if (env.getActiveProfiles().length != 0) { + log.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles()); + } + EnumSet disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC); + initMetrics(servletContext, disps); + if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + initCachingHttpHeadersFilter(servletContext, disps); + } + if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)) { + initH2Console(servletContext); + } + log.info("Web application fully configured"); + } + + /** + * Customize the Servlet engine: Mime types, the document root, the cache. + */ + @Override + public void customize(ConfigurableEmbeddedServletContainer container) { + MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT); + // IE issue, see https://github.com/jhipster/generator-jhipster/pull/711 + mappings.add("html", "text/html;charset=utf-8"); + // CloudFoundry issue, see https://github.com/cloudfoundry/gorouter/issues/64 + mappings.add("json", "text/html;charset=utf-8"); + container.setMimeMappings(mappings); + // When running in an IDE or with ./mvnw spring-boot:run, set location of the static web assets. + setLocationForStaticAssets(container); + + /* + * Enable HTTP/2 for Undertow - https://twitter.com/ankinson/status/829256167700492288 + * HTTP/2 requires HTTPS, so HTTP requests will fallback to HTTP/1.1. + * See the JHipsterProperties class and your application-*.yml configuration files + * for more information. + */ + if (jHipsterProperties.getHttp().getVersion().equals(JHipsterProperties.Http.Version.V_2_0) && + container instanceof UndertowEmbeddedServletContainerFactory) { + + ((UndertowEmbeddedServletContainerFactory) container) + .addBuilderCustomizers(builder -> + builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true)); + } + } + + private void setLocationForStaticAssets(ConfigurableEmbeddedServletContainer container) { + File root; + String prefixPath = resolvePathPrefix(); + root = new File(prefixPath + "target/www/"); + if (root.exists() && root.isDirectory()) { + container.setDocumentRoot(root); + } + } + + /** + * Resolve path prefix to static resources. + */ + private String resolvePathPrefix() { + String fullExecutablePath = this.getClass().getResource("").getPath(); + String rootPath = Paths.get(".").toUri().normalize().getPath(); + String extractedPath = fullExecutablePath.replace(rootPath, ""); + int extractionEndIndex = extractedPath.indexOf("target/"); + if(extractionEndIndex <= 0) { + return ""; + } + return extractedPath.substring(0, extractionEndIndex); + } + + /** + * Initializes the caching HTTP Headers Filter. + */ + private void initCachingHttpHeadersFilter(ServletContext servletContext, + EnumSet disps) { + log.debug("Registering Caching HTTP Headers Filter"); + FilterRegistration.Dynamic cachingHttpHeadersFilter = + servletContext.addFilter("cachingHttpHeadersFilter", + new CachingHttpHeadersFilter(jHipsterProperties)); + + cachingHttpHeadersFilter.addMappingForUrlPatterns(disps, true, "/content/*"); + cachingHttpHeadersFilter.addMappingForUrlPatterns(disps, true, "/app/*"); + cachingHttpHeadersFilter.setAsyncSupported(true); + } + + /** + * Initializes Metrics. + */ + private void initMetrics(ServletContext servletContext, EnumSet disps) { + log.debug("Initializing Metrics registries"); + servletContext.setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE, + metricRegistry); + servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY, + metricRegistry); + + log.debug("Registering Metrics Filter"); + FilterRegistration.Dynamic metricsFilter = servletContext.addFilter("webappMetricsFilter", + new InstrumentedFilter()); + + metricsFilter.addMappingForUrlPatterns(disps, true, "/*"); + metricsFilter.setAsyncSupported(true); + + log.debug("Registering Metrics Servlet"); + ServletRegistration.Dynamic metricsAdminServlet = + servletContext.addServlet("metricsServlet", new MetricsServlet()); + + metricsAdminServlet.addMapping("/management/metrics/*"); + metricsAdminServlet.setAsyncSupported(true); + metricsAdminServlet.setLoadOnStartup(2); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = jHipsterProperties.getCors(); + if (config.getAllowedOrigins() != null && !config.getAllowedOrigins().isEmpty()) { + log.debug("Registering CORS filter"); + source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/v2/api-docs", config); + } + return new CorsFilter(source); + } + + /** + * Initializes H2 console. + */ + private void initH2Console(ServletContext servletContext) { + log.debug("Initialize H2 console"); + ServletRegistration.Dynamic h2ConsoleServlet = servletContext.addServlet("H2Console", new org.h2.server.web.WebServlet()); + h2ConsoleServlet.addMapping("/h2-console/*"); + h2ConsoleServlet.setInitParameter("-properties", "src/main/resources/"); + h2ConsoleServlet.setLoadOnStartup(1); + } + + @Autowired(required = false) + public void setMetricRegistry(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/audit/AuditEventConverter.java b/jhipster/src/main/java/com/baeldung/config/audit/AuditEventConverter.java new file mode 100644 index 0000000000..2062320751 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/audit/AuditEventConverter.java @@ -0,0 +1,91 @@ +package com.baeldung.config.audit; + +import com.baeldung.domain.PersistentAuditEvent; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.*; + +@Component +public class AuditEventConverter { + + /** + * Convert a list of PersistentAuditEvent to a list of AuditEvent + * + * @param persistentAuditEvents the list to convert + * @return the converted list. + */ + public List convertToAuditEvent(Iterable persistentAuditEvents) { + if (persistentAuditEvents == null) { + return Collections.emptyList(); + } + List auditEvents = new ArrayList<>(); + for (PersistentAuditEvent persistentAuditEvent : persistentAuditEvents) { + auditEvents.add(convertToAuditEvent(persistentAuditEvent)); + } + return auditEvents; + } + + /** + * Convert a PersistentAuditEvent to an AuditEvent + * + * @param persistentAuditEvent the event to convert + * @return the converted list. + */ + public AuditEvent convertToAuditEvent(PersistentAuditEvent persistentAuditEvent) { + Instant instant = persistentAuditEvent.getAuditEventDate().atZone(ZoneId.systemDefault()).toInstant(); + return new AuditEvent(Date.from(instant), persistentAuditEvent.getPrincipal(), + persistentAuditEvent.getAuditEventType(), convertDataToObjects(persistentAuditEvent.getData())); + } + + /** + * Internal conversion. This is needed to support the current SpringBoot actuator AuditEventRepository interface + * + * @param data the data to convert + * @return a map of String, Object + */ + public Map convertDataToObjects(Map data) { + Map results = new HashMap<>(); + + if (data != null) { + for (Map.Entry entry : data.entrySet()) { + results.put(entry.getKey(), entry.getValue()); + } + } + return results; + } + + /** + * Internal conversion. This method will allow to save additional data. + * By default, it will save the object as string + * + * @param data the data to convert + * @return a map of String, String + */ + public Map convertDataToStrings(Map data) { + Map results = new HashMap<>(); + + if (data != null) { + for (Map.Entry entry : data.entrySet()) { + Object object = entry.getValue(); + + // Extract the data that will be saved. + if (object instanceof WebAuthenticationDetails) { + WebAuthenticationDetails authenticationDetails = (WebAuthenticationDetails) object; + results.put("remoteAddress", authenticationDetails.getRemoteAddress()); + results.put("sessionId", authenticationDetails.getSessionId()); + } else if (object != null) { + results.put(entry.getKey(), object.toString()); + } else { + results.put(entry.getKey(), "null"); + } + } + } + + return results; + } +} diff --git a/jhipster/src/main/java/com/baeldung/config/audit/package-info.java b/jhipster/src/main/java/com/baeldung/config/audit/package-info.java new file mode 100644 index 0000000000..8ed4f851f0 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/audit/package-info.java @@ -0,0 +1,4 @@ +/** + * Audit specific code. + */ +package com.baeldung.config.audit; diff --git a/jhipster/src/main/java/com/baeldung/config/package-info.java b/jhipster/src/main/java/com/baeldung/config/package-info.java new file mode 100644 index 0000000000..5e54ad6d03 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Framework configuration files. + */ +package com.baeldung.config; diff --git a/jhipster/src/main/java/com/baeldung/domain/AbstractAuditingEntity.java b/jhipster/src/main/java/com/baeldung/domain/AbstractAuditingEntity.java new file mode 100644 index 0000000000..2d181e5dc8 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/AbstractAuditingEntity.java @@ -0,0 +1,80 @@ +package com.baeldung.domain; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.envers.Audited; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; + +/** + * Base abstract class for entities which will hold definitions for created, last modified by and created, + * last modified by date. + */ +@MappedSuperclass +@Audited +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @CreatedBy + @Column(name = "created_by", nullable = false, length = 50, updatable = false) + @JsonIgnore + private String createdBy; + + @CreatedDate + @Column(name = "created_date", nullable = false) + @JsonIgnore + private ZonedDateTime createdDate = ZonedDateTime.now(); + + @LastModifiedBy + @Column(name = "last_modified_by", length = 50) + @JsonIgnore + private String lastModifiedBy; + + @LastModifiedDate + @Column(name = "last_modified_date") + @JsonIgnore + private ZonedDateTime lastModifiedDate = ZonedDateTime.now(); + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public ZonedDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(ZonedDateTime createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public ZonedDateTime getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(ZonedDateTime lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } +} diff --git a/jhipster/src/main/java/com/baeldung/domain/Authority.java b/jhipster/src/main/java/com/baeldung/domain/Authority.java new file mode 100644 index 0000000000..8a94ad0bb2 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/Authority.java @@ -0,0 +1,66 @@ +package com.baeldung.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Column; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * An authority (a security role) used by Spring Security. + */ +@Entity +@Table(name = "jhi_authority") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class Authority implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Size(min = 0, max = 50) + @Id + @Column(length = 50) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Authority authority = (Authority) o; + + if (name != null ? !name.equals(authority.name) : authority.name != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return "Authority{" + + "name='" + name + '\'' + + "}"; + } +} diff --git a/jhipster/src/main/java/com/baeldung/domain/Comment.java b/jhipster/src/main/java/com/baeldung/domain/Comment.java new file mode 100644 index 0000000000..c4587b6231 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/Comment.java @@ -0,0 +1,114 @@ +package com.baeldung.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import javax.persistence.*; +import javax.validation.constraints.*; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; + +/** + * A Comment. + */ +@Entity +@Table(name = "comment") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class Comment implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Size(min = 10, max = 100) + @Column(name = "text", length = 100, nullable = false) + private String text; + + @NotNull + @Column(name = "creation_date", nullable = false) + private LocalDate creationDate; + + @ManyToOne(optional = false) + @NotNull + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getText() { + return text; + } + + public Comment text(String text) { + this.text = text; + return this; + } + + public void setText(String text) { + this.text = text; + } + + public LocalDate getCreationDate() { + return creationDate; + } + + public Comment creationDate(LocalDate creationDate) { + this.creationDate = creationDate; + return this; + } + + public void setCreationDate(LocalDate creationDate) { + this.creationDate = creationDate; + } + + public Post getPost() { + return post; + } + + public Comment post(Post post) { + this.post = post; + return this; + } + + public void setPost(Post post) { + this.post = post; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Comment comment = (Comment) o; + if (comment.id == null || id == null) { + return false; + } + return Objects.equals(id, comment.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "Comment{" + + "id=" + id + + ", text='" + text + "'" + + ", creationDate='" + creationDate + "'" + + '}'; + } +} diff --git a/jhipster/src/main/java/com/baeldung/domain/PersistentAuditEvent.java b/jhipster/src/main/java/com/baeldung/domain/PersistentAuditEvent.java new file mode 100644 index 0000000000..c0fb0cb146 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/PersistentAuditEvent.java @@ -0,0 +1,78 @@ +package com.baeldung.domain; + + +import java.io.Serializable; +import java.time.LocalDateTime; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.util.HashMap; +import java.util.Map; + +/** + * Persist AuditEvent managed by the Spring Boot actuator + * @see org.springframework.boot.actuate.audit.AuditEvent + */ +@Entity +@Table(name = "jhi_persistent_audit_event") +public class PersistentAuditEvent implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long id; + + @NotNull + @Column(nullable = false) + private String principal; + + @Column(name = "event_date") + private LocalDateTime auditEventDate; + @Column(name = "event_type") + private String auditEventType; + + @ElementCollection + @MapKeyColumn(name = "name") + @Column(name = "value") + @CollectionTable(name = "jhi_persistent_audit_evt_data", joinColumns=@JoinColumn(name="event_id")) + private Map data = new HashMap<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getPrincipal() { + return principal; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + + public LocalDateTime getAuditEventDate() { + return auditEventDate; + } + + public void setAuditEventDate(LocalDateTime auditEventDate) { + this.auditEventDate = auditEventDate; + } + + public String getAuditEventType() { + return auditEventType; + } + + public void setAuditEventType(String auditEventType) { + this.auditEventType = auditEventType; + } + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } +} diff --git a/jhipster/src/main/java/com/baeldung/domain/Post.java b/jhipster/src/main/java/com/baeldung/domain/Post.java new file mode 100644 index 0000000000..7f084dc177 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/Post.java @@ -0,0 +1,133 @@ +package com.baeldung.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import javax.persistence.*; +import javax.validation.constraints.*; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; + +/** + * A Post. + */ +@Entity +@Table(name = "post") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class Post implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Size(min = 10, max = 100) + @Column(name = "title", length = 100, nullable = false) + private String title; + + @NotNull + @Size(min = 10, max = 1000) + @Column(name = "content", length = 1000, nullable = false) + private String content; + + @NotNull + @Column(name = "creation_date", nullable = false) + private LocalDate creationDate; + + @ManyToOne(optional = false) + @NotNull + private User creator; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public Post title(String title) { + this.title = title; + return this; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public Post content(String content) { + this.content = content; + return this; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDate getCreationDate() { + return creationDate; + } + + public Post creationDate(LocalDate creationDate) { + this.creationDate = creationDate; + return this; + } + + public void setCreationDate(LocalDate creationDate) { + this.creationDate = creationDate; + } + + public User getCreator() { + return creator; + } + + public Post creator(User user) { + this.creator = user; + return this; + } + + public void setCreator(User user) { + this.creator = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Post post = (Post) o; + if (post.id == null || id == null) { + return false; + } + return Objects.equals(id, post.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "Post{" + + "id=" + id + + ", title='" + title + "'" + + ", content='" + content + "'" + + ", creationDate='" + creationDate + "'" + + '}'; + } +} diff --git a/jhipster/src/main/java/com/baeldung/domain/User.java b/jhipster/src/main/java/com/baeldung/domain/User.java new file mode 100644 index 0000000000..76aa3a5209 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/User.java @@ -0,0 +1,235 @@ +package com.baeldung.domain; + +import com.baeldung.config.Constants; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.validator.constraints.Email; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.time.ZonedDateTime; + +/** + * A user. + */ +@Entity +@Table(name = "jhi_user") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class User extends AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + @Column(length = 50, unique = true, nullable = false) + private String login; + + @JsonIgnore + @NotNull + @Size(min = 60, max = 60) + @Column(name = "password_hash",length = 60) + private String password; + + @Size(max = 50) + @Column(name = "first_name", length = 50) + private String firstName; + + @Size(max = 50) + @Column(name = "last_name", length = 50) + private String lastName; + + @Email + @Size(max = 100) + @Column(length = 100, unique = true) + private String email; + + @NotNull + @Column(nullable = false) + private boolean activated = false; + + @Size(min = 2, max = 5) + @Column(name = "lang_key", length = 5) + private String langKey; + + @Size(max = 256) + @Column(name = "image_url", length = 256) + private String imageUrl; + + @Size(max = 20) + @Column(name = "activation_key", length = 20) + @JsonIgnore + private String activationKey; + + @Size(max = 20) + @Column(name = "reset_key", length = 20) + private String resetKey; + + @Column(name = "reset_date") + private ZonedDateTime resetDate = null; + + @JsonIgnore + @ManyToMany + @JoinTable( + name = "jhi_user_authority", + joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @BatchSize(size = 20) + private Set authorities = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + //Lowercase the login before saving it in database + public void setLogin(String login) { + this.login = login.toLowerCase(Locale.ENGLISH); + } + + 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 String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + 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 String getResetKey() { + return resetKey; + } + + public void setResetKey(String resetKey) { + this.resetKey = resetKey; + } + + public ZonedDateTime getResetDate() { + return resetDate; + } + + public void setResetDate(ZonedDateTime resetDate) { + this.resetDate = resetDate; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + User user = (User) o; + + if (!login.equals(user.login)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return login.hashCode(); + } + + @Override + public String toString() { + return "User{" + + "login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", activated='" + activated + '\'' + + ", langKey='" + langKey + '\'' + + ", activationKey='" + activationKey + '\'' + + "}"; + } +} diff --git a/jhipster/src/main/java/com/baeldung/domain/package-info.java b/jhipster/src/main/java/com/baeldung/domain/package-info.java new file mode 100644 index 0000000000..2492048c77 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/domain/package-info.java @@ -0,0 +1,4 @@ +/** + * JPA domain objects. + */ +package com.baeldung.domain; diff --git a/jhipster/src/main/java/com/baeldung/repository/AuthorityRepository.java b/jhipster/src/main/java/com/baeldung/repository/AuthorityRepository.java new file mode 100644 index 0000000000..fab63174aa --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/AuthorityRepository.java @@ -0,0 +1,11 @@ +package com.baeldung.repository; + +import com.baeldung.domain.Authority; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Spring Data JPA repository for the Authority entity. + */ +public interface AuthorityRepository extends JpaRepository { +} diff --git a/jhipster/src/main/java/com/baeldung/repository/CommentRepository.java b/jhipster/src/main/java/com/baeldung/repository/CommentRepository.java new file mode 100644 index 0000000000..d2d2618c36 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/CommentRepository.java @@ -0,0 +1,15 @@ +package com.baeldung.repository; + +import com.baeldung.domain.Comment; + +import org.springframework.data.jpa.repository.*; + +import java.util.List; + +/** + * Spring Data JPA repository for the Comment entity. + */ +@SuppressWarnings("unused") +public interface CommentRepository extends JpaRepository { + +} diff --git a/jhipster/src/main/java/com/baeldung/repository/CustomAuditEventRepository.java b/jhipster/src/main/java/com/baeldung/repository/CustomAuditEventRepository.java new file mode 100644 index 0000000000..fcfa9baeec --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/CustomAuditEventRepository.java @@ -0,0 +1,81 @@ +package com.baeldung.repository; + +import com.baeldung.config.Constants; +import com.baeldung.config.audit.AuditEventConverter; +import com.baeldung.domain.PersistentAuditEvent; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; + +/** + * An implementation of Spring Boot's AuditEventRepository. + */ +@Repository +public class CustomAuditEventRepository implements AuditEventRepository { + + private static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE"; + + private final PersistenceAuditEventRepository persistenceAuditEventRepository; + + private final AuditEventConverter auditEventConverter; + + public CustomAuditEventRepository(PersistenceAuditEventRepository persistenceAuditEventRepository, + AuditEventConverter auditEventConverter) { + + this.persistenceAuditEventRepository = persistenceAuditEventRepository; + this.auditEventConverter = auditEventConverter; + } + + @Override + public List find(Date after) { + Iterable persistentAuditEvents = + persistenceAuditEventRepository.findByAuditEventDateAfter(LocalDateTime.from(after.toInstant())); + return auditEventConverter.convertToAuditEvent(persistentAuditEvents); + } + + @Override + public List find(String principal, Date after) { + Iterable persistentAuditEvents; + if (principal == null && after == null) { + persistentAuditEvents = persistenceAuditEventRepository.findAll(); + } else if (after == null) { + persistentAuditEvents = persistenceAuditEventRepository.findByPrincipal(principal); + } else { + persistentAuditEvents = + persistenceAuditEventRepository.findByPrincipalAndAuditEventDateAfter(principal, LocalDateTime.from(after.toInstant())); + } + return auditEventConverter.convertToAuditEvent(persistentAuditEvents); + } + + @Override + public List find(String principal, Date after, String type) { + Iterable persistentAuditEvents = + persistenceAuditEventRepository.findByPrincipalAndAuditEventDateAfterAndAuditEventType(principal, LocalDateTime.from(after.toInstant()), type); + return auditEventConverter.convertToAuditEvent(persistentAuditEvents); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void add(AuditEvent event) { + if (!AUTHORIZATION_FAILURE.equals(event.getType()) && + !Constants.ANONYMOUS_USER.equals(event.getPrincipal())) { + + PersistentAuditEvent persistentAuditEvent = new PersistentAuditEvent(); + persistentAuditEvent.setPrincipal(event.getPrincipal()); + persistentAuditEvent.setAuditEventType(event.getType()); + Instant instant = Instant.ofEpochMilli(event.getTimestamp().getTime()); + persistentAuditEvent.setAuditEventDate(LocalDateTime.ofInstant(instant, ZoneId.systemDefault())); + persistentAuditEvent.setData(auditEventConverter.convertDataToStrings(event.getData())); + persistenceAuditEventRepository.save(persistentAuditEvent); + } + } +} diff --git a/jhipster/src/main/java/com/baeldung/repository/PersistenceAuditEventRepository.java b/jhipster/src/main/java/com/baeldung/repository/PersistenceAuditEventRepository.java new file mode 100644 index 0000000000..f4b7f1c5bf --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/PersistenceAuditEventRepository.java @@ -0,0 +1,26 @@ +package com.baeldung.repository; + +import com.baeldung.domain.PersistentAuditEvent; + +import java.time.LocalDateTime; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * Spring Data JPA repository for the PersistentAuditEvent entity. + */ +public interface PersistenceAuditEventRepository extends JpaRepository { + + List findByPrincipal(String principal); + + List findByAuditEventDateAfter(LocalDateTime after); + + List findByPrincipalAndAuditEventDateAfter(String principal, LocalDateTime after); + + List findByPrincipalAndAuditEventDateAfterAndAuditEventType(String principle, LocalDateTime after, String type); + + Page findAllByAuditEventDateBetween(LocalDateTime fromDate, LocalDateTime toDate, Pageable pageable); +} diff --git a/jhipster/src/main/java/com/baeldung/repository/PostRepository.java b/jhipster/src/main/java/com/baeldung/repository/PostRepository.java new file mode 100644 index 0000000000..22e8aa1372 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/PostRepository.java @@ -0,0 +1,18 @@ +package com.baeldung.repository; + +import com.baeldung.domain.Post; + +import org.springframework.data.jpa.repository.*; + +import java.util.List; + +/** + * Spring Data JPA repository for the Post entity. + */ +@SuppressWarnings("unused") +public interface PostRepository extends JpaRepository { + + @Query("select post from Post post where post.creator.login = ?#{principal.username}") + List findByCreatorIsCurrentUser(); + +} diff --git a/jhipster/src/main/java/com/baeldung/repository/UserRepository.java b/jhipster/src/main/java/com/baeldung/repository/UserRepository.java new file mode 100644 index 0000000000..a8f8149910 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/UserRepository.java @@ -0,0 +1,37 @@ +package com.baeldung.repository; + +import com.baeldung.domain.User; + +import java.time.ZonedDateTime; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +/** + * Spring Data JPA repository for the User entity. + */ +public interface UserRepository extends JpaRepository { + + Optional findOneByActivationKey(String activationKey); + + List findAllByActivatedIsFalseAndCreatedDateBefore(ZonedDateTime dateTime); + + Optional findOneByResetKey(String resetKey); + + Optional findOneByEmail(String email); + + Optional findOneByLogin(String login); + + @EntityGraph(attributePaths = "authorities") + User findOneWithAuthoritiesById(Long id); + + @EntityGraph(attributePaths = "authorities") + Optional findOneWithAuthoritiesByLogin(String login); + + Page findAllByLoginNot(Pageable pageable, String login); +} diff --git a/jhipster/src/main/java/com/baeldung/repository/package-info.java b/jhipster/src/main/java/com/baeldung/repository/package-info.java new file mode 100644 index 0000000000..9d1fb837c3 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Data JPA repositories. + */ +package com.baeldung.repository; diff --git a/jhipster/src/main/java/com/baeldung/security/AuthoritiesConstants.java b/jhipster/src/main/java/com/baeldung/security/AuthoritiesConstants.java new file mode 100644 index 0000000000..40cf54e4f6 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/AuthoritiesConstants.java @@ -0,0 +1,16 @@ +package com.baeldung.security; + +/** + * Constants for Spring Security authorities. + */ +public final class AuthoritiesConstants { + + public static final String ADMIN = "ROLE_ADMIN"; + + public static final String USER = "ROLE_USER"; + + public static final String ANONYMOUS = "ROLE_ANONYMOUS"; + + private AuthoritiesConstants() { + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/DomainUserDetailsService.java b/jhipster/src/main/java/com/baeldung/security/DomainUserDetailsService.java new file mode 100644 index 0000000000..6ce07739f7 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/DomainUserDetailsService.java @@ -0,0 +1,51 @@ +package com.baeldung.security; + +import com.baeldung.domain.User; +import com.baeldung.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.GrantedAuthority; +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.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Authenticate a user from the database. + */ +@Component("userDetailsService") +public class DomainUserDetailsService implements UserDetailsService { + + private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class); + + private final UserRepository userRepository; + + public DomainUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + @Transactional + public UserDetails loadUserByUsername(final String login) { + log.debug("Authenticating {}", login); + String lowercaseLogin = login.toLowerCase(Locale.ENGLISH); + Optional userFromDatabase = userRepository.findOneWithAuthoritiesByLogin(lowercaseLogin); + return userFromDatabase.map(user -> { + if (!user.getActivated()) { + throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated"); + } + List grantedAuthorities = user.getAuthorities().stream() + .map(authority -> new SimpleGrantedAuthority(authority.getName())) + .collect(Collectors.toList()); + return new org.springframework.security.core.userdetails.User(lowercaseLogin, + user.getPassword(), + grantedAuthorities); + }).orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the " + + "database")); + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/SecurityUtils.java b/jhipster/src/main/java/com/baeldung/security/SecurityUtils.java new file mode 100644 index 0000000000..96aaef3805 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/SecurityUtils.java @@ -0,0 +1,68 @@ +package com.baeldung.security; + +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; + +/** + * Utility class for Spring Security. + */ +public final class SecurityUtils { + + private SecurityUtils() { + } + + /** + * Get the login of the current user. + * + * @return the login of the current user + */ + public static String getCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication authentication = securityContext.getAuthentication(); + String userName = null; + if (authentication != null) { + if (authentication.getPrincipal() instanceof UserDetails) { + UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); + userName = springSecurityUser.getUsername(); + } else if (authentication.getPrincipal() instanceof String) { + userName = (String) authentication.getPrincipal(); + } + } + return userName; + } + + /** + * Check if a user is authenticated. + * + * @return true if the user is authenticated, false otherwise + */ + public static boolean isAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication authentication = securityContext.getAuthentication(); + if (authentication != null) { + return authentication.getAuthorities().stream() + .noneMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(AuthoritiesConstants.ANONYMOUS)); + } + return false; + } + + /** + * If the current user has a specific authority (security role). + * + *

The name of this method comes from the isUserInRole() method in the Servlet API

+ * + * @param authority the authority to check + * @return true if the current user has the authority, false otherwise + */ + public static boolean isCurrentUserInRole(String authority) { + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication authentication = securityContext.getAuthentication(); + if (authentication != null) { + return authentication.getAuthorities().stream() + .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(authority)); + } + return false; + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/SpringSecurityAuditorAware.java b/jhipster/src/main/java/com/baeldung/security/SpringSecurityAuditorAware.java new file mode 100644 index 0000000000..d59917153c --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/SpringSecurityAuditorAware.java @@ -0,0 +1,19 @@ +package com.baeldung.security; + +import com.baeldung.config.Constants; + +import org.springframework.data.domain.AuditorAware; +import org.springframework.stereotype.Component; + +/** + * Implementation of AuditorAware based on Spring Security. + */ +@Component +public class SpringSecurityAuditorAware implements AuditorAware { + + @Override + public String getCurrentAuditor() { + String userName = SecurityUtils.getCurrentUserLogin(); + return userName != null ? userName : Constants.SYSTEM_ACCOUNT; + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/UserNotActivatedException.java b/jhipster/src/main/java/com/baeldung/security/UserNotActivatedException.java new file mode 100644 index 0000000000..7265188cad --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/UserNotActivatedException.java @@ -0,0 +1,19 @@ +package com.baeldung.security; + +import org.springframework.security.core.AuthenticationException; + +/** + * This exception is thrown in case of a not activated user trying to authenticate. + */ +public class UserNotActivatedException extends AuthenticationException { + + private static final long serialVersionUID = 1L; + + public UserNotActivatedException(String message) { + super(message); + } + + public UserNotActivatedException(String message, Throwable t) { + super(message, t); + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/jwt/JWTConfigurer.java b/jhipster/src/main/java/com/baeldung/security/jwt/JWTConfigurer.java new file mode 100644 index 0000000000..5720812b9f --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/jwt/JWTConfigurer.java @@ -0,0 +1,23 @@ +package com.baeldung.security.jwt; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JWTConfigurer extends SecurityConfigurerAdapter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private TokenProvider tokenProvider; + + public JWTConfigurer(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void configure(HttpSecurity http) throws Exception { + JWTFilter customFilter = new JWTFilter(tokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/jwt/JWTFilter.java b/jhipster/src/main/java/com/baeldung/security/jwt/JWTFilter.java new file mode 100644 index 0000000000..89b1947e05 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/jwt/JWTFilter.java @@ -0,0 +1,58 @@ +package com.baeldung.security.jwt; + +import java.io.IOException; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import io.jsonwebtoken.ExpiredJwtException; + +/** + * Filters incoming requests and installs a Spring Security principal if a header corresponding to a valid user is + * found. + */ +public class JWTFilter extends GenericFilterBean { + + private final Logger log = LoggerFactory.getLogger(JWTFilter.class); + + private TokenProvider tokenProvider; + + public JWTFilter(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + try { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String jwt = resolveToken(httpServletRequest); + if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) { + Authentication authentication = this.tokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(servletRequest, servletResponse); + } catch (ExpiredJwtException eje) { + log.info("Security exception for user {} - {}", + eje.getClaims().getSubject(), eje.getMessage()); + + log.trace("Security exception trace: {}", eje); + ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private String resolveToken(HttpServletRequest request){ + String bearerToken = request.getHeader(JWTConfigurer.AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7, bearerToken.length()); + } + return null; + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/jwt/TokenProvider.java b/jhipster/src/main/java/com/baeldung/security/jwt/TokenProvider.java new file mode 100644 index 0000000000..3ba4d7c793 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/jwt/TokenProvider.java @@ -0,0 +1,109 @@ +package com.baeldung.security.jwt; + +import io.github.jhipster.config.JHipsterProperties; + +import java.util.*; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.*; + +@Component +public class TokenProvider { + + private final Logger log = LoggerFactory.getLogger(TokenProvider.class); + + private static final String AUTHORITIES_KEY = "auth"; + + private String secretKey; + + private long tokenValidityInMilliseconds; + + private long tokenValidityInMillisecondsForRememberMe; + + private final JHipsterProperties jHipsterProperties; + + public TokenProvider(JHipsterProperties jHipsterProperties) { + this.jHipsterProperties = jHipsterProperties; + } + + @PostConstruct + public void init() { + this.secretKey = + jHipsterProperties.getSecurity().getAuthentication().getJwt().getSecret(); + + this.tokenValidityInMilliseconds = + 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSeconds(); + this.tokenValidityInMillisecondsForRememberMe = + 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe(); + } + + public String createToken(Authentication authentication, Boolean rememberMe) { + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + Date validity; + if (rememberMe) { + validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe); + } else { + validity = new Date(now + this.tokenValidityInMilliseconds); + } + + return Jwts.builder() + .setSubject(authentication.getName()) + .claim(AUTHORITIES_KEY, authorities) + .signWith(SignatureAlgorithm.HS512, secretKey) + .setExpiration(validity) + .compact(); + } + + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + + Collection authorities = + Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + User principal = new User(claims.getSubject(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + public boolean validateToken(String authToken) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken); + return true; + } catch (SignatureException e) { + log.info("Invalid JWT signature."); + log.trace("Invalid JWT signature trace: {}", e); + } catch (MalformedJwtException e) { + log.info("Invalid JWT token."); + log.trace("Invalid JWT token trace: {}", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token."); + log.trace("Expired JWT token trace: {}", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT token."); + log.trace("Unsupported JWT token trace: {}", e); + } catch (IllegalArgumentException e) { + log.info("JWT token compact of handler are invalid."); + log.trace("JWT token compact of handler are invalid trace: {}", e); + } + return false; + } +} diff --git a/jhipster/src/main/java/com/baeldung/security/package-info.java b/jhipster/src/main/java/com/baeldung/security/package-info.java new file mode 100644 index 0000000000..1e29c15b4e --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/security/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Security configuration. + */ +package com.baeldung.security; diff --git a/jhipster/src/main/java/com/baeldung/service/AuditEventService.java b/jhipster/src/main/java/com/baeldung/service/AuditEventService.java new file mode 100644 index 0000000000..8701854922 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/AuditEventService.java @@ -0,0 +1,50 @@ +package com.baeldung.service; + +import com.baeldung.config.audit.AuditEventConverter; +import com.baeldung.repository.PersistenceAuditEventRepository; +import java.time.LocalDateTime; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/** + * Service for managing audit events. + *

+ * This is the default implementation to support SpringBoot Actuator AuditEventRepository + *

+ */ +@Service +@Transactional +public class AuditEventService { + + private final PersistenceAuditEventRepository persistenceAuditEventRepository; + + private final AuditEventConverter auditEventConverter; + + public AuditEventService( + PersistenceAuditEventRepository persistenceAuditEventRepository, + AuditEventConverter auditEventConverter) { + + this.persistenceAuditEventRepository = persistenceAuditEventRepository; + this.auditEventConverter = auditEventConverter; + } + + public Page findAll(Pageable pageable) { + return persistenceAuditEventRepository.findAll(pageable) + .map(auditEventConverter::convertToAuditEvent); + } + + public Page findByDates(LocalDateTime fromDate, LocalDateTime toDate, Pageable pageable) { + return persistenceAuditEventRepository.findAllByAuditEventDateBetween(fromDate, toDate, pageable) + .map(auditEventConverter::convertToAuditEvent); + } + + public Optional find(Long id) { + return Optional.ofNullable(persistenceAuditEventRepository.findOne(id)).map + (auditEventConverter::convertToAuditEvent); + } +} diff --git a/jhipster/src/main/java/com/baeldung/service/MailService.java b/jhipster/src/main/java/com/baeldung/service/MailService.java new file mode 100644 index 0000000000..9f85531da7 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/MailService.java @@ -0,0 +1,108 @@ +package com.baeldung.service; + +import com.baeldung.domain.User; + +import io.github.jhipster.config.JHipsterProperties; + +import org.apache.commons.lang3.CharEncoding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +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 javax.mail.internet.MimeMessage; +import java.util.Locale; + +/** + * Service for sending e-mails. + *

+ * We use the @Async annotation to send e-mails asynchronously. + *

+ */ +@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 JHipsterProperties jHipsterProperties; + + private final JavaMailSender javaMailSender; + + private final MessageSource messageSource; + + private final SpringTemplateEngine templateEngine; + + public MailService(JHipsterProperties jHipsterProperties, JavaMailSender javaMailSender, + MessageSource messageSource, SpringTemplateEngine templateEngine) { + + this.jHipsterProperties = jHipsterProperties; + this.javaMailSender = javaMailSender; + this.messageSource = messageSource; + this.templateEngine = templateEngine; + } + + @Async + public void sendEmail(String to, String subject, String content, boolean isMultipart, boolean isHtml) { + log.debug("Send e-mail[multipart '{}' and html '{}'] to '{}' with subject '{}' and content={}", + isMultipart, isHtml, to, subject, content); + + // Prepare message using a Spring helper + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper message = new MimeMessageHelper(mimeMessage, isMultipart, CharEncoding.UTF_8); + message.setTo(to); + message.setFrom(jHipsterProperties.getMail().getFrom()); + message.setSubject(subject); + message.setText(content, isHtml); + javaMailSender.send(mimeMessage); + log.debug("Sent e-mail to User '{}'", to); + } catch (Exception e) { + log.warn("E-mail could not be sent to user '{}'", to, e); + } + } + + @Async + public void sendActivationEmail(User user) { + log.debug("Sending activation e-mail to '{}'", user.getEmail()); + Locale locale = Locale.forLanguageTag(user.getLangKey()); + Context context = new Context(locale); + context.setVariable(USER, user); + context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl()); + String content = templateEngine.process("activationEmail", context); + String subject = messageSource.getMessage("email.activation.title", null, locale); + sendEmail(user.getEmail(), subject, content, false, true); + } + + @Async + public void sendCreationEmail(User user) { + log.debug("Sending creation e-mail to '{}'", user.getEmail()); + Locale locale = Locale.forLanguageTag(user.getLangKey()); + Context context = new Context(locale); + context.setVariable(USER, user); + context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl()); + String content = templateEngine.process("creationEmail", context); + String subject = messageSource.getMessage("email.activation.title", null, locale); + sendEmail(user.getEmail(), subject, content, false, true); + } + + @Async + public void sendPasswordResetMail(User user) { + log.debug("Sending password reset e-mail to '{}'", user.getEmail()); + Locale locale = Locale.forLanguageTag(user.getLangKey()); + Context context = new Context(locale); + context.setVariable(USER, user); + context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl()); + String content = templateEngine.process("passwordResetEmail", context); + String subject = messageSource.getMessage("email.reset.title", null, locale); + sendEmail(user.getEmail(), subject, content, false, true); + } +} diff --git a/jhipster/src/main/java/com/baeldung/service/UserService.java b/jhipster/src/main/java/com/baeldung/service/UserService.java new file mode 100644 index 0000000000..7b9096e0da --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/UserService.java @@ -0,0 +1,228 @@ +package com.baeldung.service; + +import com.baeldung.domain.Authority; +import com.baeldung.domain.User; +import com.baeldung.repository.AuthorityRepository; +import com.baeldung.config.Constants; +import com.baeldung.repository.UserRepository; +import com.baeldung.security.AuthoritiesConstants; +import com.baeldung.security.SecurityUtils; +import com.baeldung.service.util.RandomUtil; +import com.baeldung.service.dto.UserDTO; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.ZonedDateTime; +import java.util.*; + +/** + * Service class for managing users. + */ +@Service +@Transactional +public class UserService { + + private final Logger log = LoggerFactory.getLogger(UserService.class); + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + private final AuthorityRepository authorityRepository; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthorityRepository authorityRepository) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.authorityRepository = authorityRepository; + } + + public Optional activateRegistration(String key) { + log.debug("Activating user for activation key {}", key); + return userRepository.findOneByActivationKey(key) + .map(user -> { + // activate given user for the registration key. + user.setActivated(true); + user.setActivationKey(null); + log.debug("Activated user: {}", user); + return user; + }); + } + + public Optional completePasswordReset(String newPassword, String key) { + log.debug("Reset user password for reset key {}", key); + + return userRepository.findOneByResetKey(key) + .filter(user -> { + ZonedDateTime oneDayAgo = ZonedDateTime.now().minusHours(24); + return user.getResetDate().isAfter(oneDayAgo); + }) + .map(user -> { + user.setPassword(passwordEncoder.encode(newPassword)); + user.setResetKey(null); + user.setResetDate(null); + return user; + }); + } + + public Optional requestPasswordReset(String mail) { + return userRepository.findOneByEmail(mail) + .filter(User::getActivated) + .map(user -> { + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(ZonedDateTime.now()); + return user; + }); + } + + public User createUser(String login, String password, String firstName, String lastName, String email, + String imageUrl, String langKey) { + + User newUser = new User(); + Authority authority = authorityRepository.findOne(AuthoritiesConstants.USER); + Set authorities = new HashSet<>(); + String encryptedPassword = passwordEncoder.encode(password); + newUser.setLogin(login); + // new user gets initially a generated password + newUser.setPassword(encryptedPassword); + newUser.setFirstName(firstName); + newUser.setLastName(lastName); + newUser.setEmail(email); + newUser.setImageUrl(imageUrl); + newUser.setLangKey(langKey); + // new user is not active + newUser.setActivated(false); + // new user gets registration key + newUser.setActivationKey(RandomUtil.generateActivationKey()); + authorities.add(authority); + newUser.setAuthorities(authorities); + userRepository.save(newUser); + log.debug("Created Information for User: {}", newUser); + return newUser; + } + + public User createUser(UserDTO userDTO) { + User user = new User(); + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + if (userDTO.getLangKey() == null) { + user.setLangKey("en"); // default language + } else { + user.setLangKey(userDTO.getLangKey()); + } + if (userDTO.getAuthorities() != null) { + Set authorities = new HashSet<>(); + userDTO.getAuthorities().forEach( + authority -> authorities.add(authorityRepository.findOne(authority)) + ); + user.setAuthorities(authorities); + } + String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword()); + user.setPassword(encryptedPassword); + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(ZonedDateTime.now()); + user.setActivated(true); + userRepository.save(user); + log.debug("Created Information for User: {}", user); + return user; + } + + /** + * Update basic information (first name, last name, email, language) for the current user. + */ + public void updateUser(String firstName, String lastName, String email, String langKey) { + userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin()).ifPresent(user -> { + user.setFirstName(firstName); + user.setLastName(lastName); + user.setEmail(email); + user.setLangKey(langKey); + log.debug("Changed Information for User: {}", user); + }); + } + + /** + * Update all information for a specific user, and return the modified user. + */ + public Optional updateUser(UserDTO userDTO) { + return Optional.of(userRepository + .findOne(userDTO.getId())) + .map(user -> { + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set managedAuthorities = user.getAuthorities(); + managedAuthorities.clear(); + userDTO.getAuthorities().stream() + .map(authorityRepository::findOne) + .forEach(managedAuthorities::add); + log.debug("Changed Information for User: {}", user); + return user; + }) + .map(UserDTO::new); + } + + public void deleteUser(String login) { + userRepository.findOneByLogin(login).ifPresent(user -> { + userRepository.delete(user); + log.debug("Deleted User: {}", user); + }); + } + + public void changePassword(String password) { + userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin()).ifPresent(user -> { + String encryptedPassword = passwordEncoder.encode(password); + user.setPassword(encryptedPassword); + log.debug("Changed password for User: {}", user); + }); + } + + @Transactional(readOnly = true) + public Page getAllManagedUsers(Pageable pageable) { + return userRepository.findAllByLoginNot(pageable, Constants.ANONYMOUS_USER).map(UserDTO::new); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthoritiesByLogin(String login) { + return userRepository.findOneWithAuthoritiesByLogin(login); + } + + @Transactional(readOnly = true) + public User getUserWithAuthorities(Long id) { + return userRepository.findOneWithAuthoritiesById(id); + } + + @Transactional(readOnly = true) + public User getUserWithAuthorities() { + return userRepository.findOneWithAuthoritiesByLogin(SecurityUtils.getCurrentUserLogin()).orElse(null); + } + + + /** + * Not activated users should be automatically deleted after 3 days. + *

+ * This is scheduled to get fired everyday, at 01:00 (am). + *

+ */ + @Scheduled(cron = "0 0 1 * * ?") + public void removeNotActivatedUsers() { + ZonedDateTime now = ZonedDateTime.now(); + List users = userRepository.findAllByActivatedIsFalseAndCreatedDateBefore(now.minusDays(3)); + for (User user : users) { + log.debug("Deleting not activated user {}", user.getLogin()); + userRepository.delete(user); + } + } +} diff --git a/jhipster/src/main/java/com/baeldung/service/dto/UserDTO.java b/jhipster/src/main/java/com/baeldung/service/dto/UserDTO.java new file mode 100644 index 0000000000..1080bab7b8 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/dto/UserDTO.java @@ -0,0 +1,167 @@ +package com.baeldung.service.dto; + +import com.baeldung.config.Constants; + +import com.baeldung.domain.Authority; +import com.baeldung.domain.User; + +import org.hibernate.validator.constraints.Email; + +import javax.validation.constraints.*; +import java.time.ZonedDateTime; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A DTO representing a user, with his authorities. + */ +public class UserDTO { + + private Long id; + + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + private String login; + + @Size(max = 50) + private String firstName; + + @Size(max = 50) + private String lastName; + + @Email + @Size(min = 5, max = 100) + private String email; + + @Size(max = 256) + private String imageUrl; + + private boolean activated = false; + + @Size(min = 2, max = 5) + private String langKey; + + private String createdBy; + + private ZonedDateTime createdDate; + + private String lastModifiedBy; + + private ZonedDateTime lastModifiedDate; + + private Set authorities; + + public UserDTO() { + // Empty constructor needed for MapStruct. + } + + public UserDTO(User user) { + this(user.getId(), user.getLogin(), user.getFirstName(), user.getLastName(), + user.getEmail(), user.getActivated(), user.getImageUrl(), user.getLangKey(), + user.getCreatedBy(), user.getCreatedDate(), user.getLastModifiedBy(), user.getLastModifiedDate(), + user.getAuthorities().stream().map(Authority::getName) + .collect(Collectors.toSet())); + } + + public UserDTO(Long id, String login, String firstName, String lastName, + String email, boolean activated, String imageUrl, String langKey, + String createdBy, ZonedDateTime createdDate, String lastModifiedBy, ZonedDateTime lastModifiedDate, + Set authorities) { + + this.id = id; + this.login = login; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.activated = activated; + this.imageUrl = imageUrl; + this.langKey = langKey; + this.createdBy = createdBy; + this.createdDate = createdDate; + this.lastModifiedBy = lastModifiedBy; + this.lastModifiedDate = lastModifiedDate; + this.authorities = authorities; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getEmail() { + return email; + } + + public String getImageUrl() { + return imageUrl; + } + + public boolean isActivated() { + return activated; + } + + public String getLangKey() { + return langKey; + } + + public String getCreatedBy() { + return createdBy; + } + + public ZonedDateTime getCreatedDate() { + return createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public ZonedDateTime getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(ZonedDateTime lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public Set getAuthorities() { + return authorities; + } + + @Override + public String toString() { + return "UserDTO{" + + "login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", activated=" + activated + + ", langKey='" + langKey + '\'' + + ", createdBy=" + createdBy + + ", createdDate=" + createdDate + + ", lastModifiedBy='" + lastModifiedBy + '\'' + + ", lastModifiedDate=" + lastModifiedDate + + ", authorities=" + authorities + + "}"; + } +} diff --git a/jhipster/src/main/java/com/baeldung/service/dto/package-info.java b/jhipster/src/main/java/com/baeldung/service/dto/package-info.java new file mode 100644 index 0000000000..12d048eb7a --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * Data Transfer Objects. + */ +package com.baeldung.service.dto; diff --git a/jhipster/src/main/java/com/baeldung/service/mapper/UserMapper.java b/jhipster/src/main/java/com/baeldung/service/mapper/UserMapper.java new file mode 100644 index 0000000000..aaada37162 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/mapper/UserMapper.java @@ -0,0 +1,55 @@ +package com.baeldung.service.mapper; + +import com.baeldung.domain.Authority; +import com.baeldung.domain.User; +import com.baeldung.service.dto.UserDTO; +import org.mapstruct.*; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Mapper for the entity User and its DTO UserDTO. + */ +@Mapper(componentModel = "spring", uses = {}) +public interface UserMapper { + + UserDTO userToUserDTO(User user); + + List usersToUserDTOs(List users); + + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "lastModifiedBy", ignore = true) + @Mapping(target = "lastModifiedDate", ignore = true) + @Mapping(target = "activationKey", ignore = true) + @Mapping(target = "resetKey", ignore = true) + @Mapping(target = "resetDate", ignore = true) + @Mapping(target = "password", ignore = true) + User userDTOToUser(UserDTO userDTO); + + List userDTOsToUsers(List userDTOs); + + default User userFromId(Long id) { + if (id == null) { + return null; + } + User user = new User(); + user.setId(id); + return user; + } + + default Set stringsFromAuthorities (Set authorities) { + return authorities.stream().map(Authority::getName) + .collect(Collectors.toSet()); + } + + default Set authoritiesFromStrings(Set strings) { + return strings.stream().map(string -> { + Authority auth = new Authority(); + auth.setName(string); + return auth; + }).collect(Collectors.toSet()); + } +} diff --git a/jhipster/src/main/java/com/baeldung/service/mapper/package-info.java b/jhipster/src/main/java/com/baeldung/service/mapper/package-info.java new file mode 100644 index 0000000000..75f8d24f08 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * MapStruct mappers for mapping domain objects and Data Transfer Objects. + */ +package com.baeldung.service.mapper; diff --git a/jhipster/src/main/java/com/baeldung/service/package-info.java b/jhipster/src/main/java/com/baeldung/service/package-info.java new file mode 100644 index 0000000000..0695bc1473 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/package-info.java @@ -0,0 +1,4 @@ +/** + * Service layer beans. + */ +package com.baeldung.service; diff --git a/jhipster/src/main/java/com/baeldung/service/util/RandomUtil.java b/jhipster/src/main/java/com/baeldung/service/util/RandomUtil.java new file mode 100644 index 0000000000..1f4d9f8b5d --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/service/util/RandomUtil.java @@ -0,0 +1,41 @@ +package com.baeldung.service.util; + +import org.apache.commons.lang3.RandomStringUtils; + +/** + * Utility class for generating random Strings. + */ +public final class RandomUtil { + + private static final int DEF_COUNT = 20; + + private RandomUtil() { + } + + /** + * Generate a password. + * + * @return the generated password + */ + public static String generatePassword() { + return RandomStringUtils.randomAlphanumeric(DEF_COUNT); + } + + /** + * Generate an activation key. + * + * @return the generated activation key + */ + public static String generateActivationKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + + /** + * Generate a reset key. + * + * @return the generated reset key + */ + public static String generateResetKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/AccountResource.java b/jhipster/src/main/java/com/baeldung/web/rest/AccountResource.java new file mode 100644 index 0000000000..c1009fc918 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/AccountResource.java @@ -0,0 +1,202 @@ +package com.baeldung.web.rest; + +import com.codahale.metrics.annotation.Timed; + +import com.baeldung.domain.User; +import com.baeldung.repository.UserRepository; +import com.baeldung.security.SecurityUtils; +import com.baeldung.service.MailService; +import com.baeldung.service.UserService; +import com.baeldung.service.dto.UserDTO; +import com.baeldung.web.rest.vm.KeyAndPasswordVM; +import com.baeldung.web.rest.vm.ManagedUserVM; +import com.baeldung.web.rest.util.HeaderUtil; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.*; + +/** + * REST controller for managing the current user's account. + */ +@RestController +@RequestMapping("/api") +public class AccountResource { + + private final Logger log = LoggerFactory.getLogger(AccountResource.class); + + private final UserRepository userRepository; + + private final UserService userService; + + private final MailService mailService; + + public AccountResource(UserRepository userRepository, UserService userService, + MailService mailService) { + + this.userRepository = userRepository; + this.userService = userService; + this.mailService = mailService; + } + + /** + * POST /register : register the user. + * + * @param managedUserVM the managed user View Model + * @return the ResponseEntity with status 201 (Created) if the user is registered or 400 (Bad Request) if the login or e-mail is already in use + */ + @PostMapping(path = "/register", + produces={MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE}) + @Timed + public ResponseEntity registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) { + + HttpHeaders textPlainHeaders = new HttpHeaders(); + textPlainHeaders.setContentType(MediaType.TEXT_PLAIN); + + return userRepository.findOneByLogin(managedUserVM.getLogin().toLowerCase()) + .map(user -> new ResponseEntity<>("login already in use", textPlainHeaders, HttpStatus.BAD_REQUEST)) + .orElseGet(() -> userRepository.findOneByEmail(managedUserVM.getEmail()) + .map(user -> new ResponseEntity<>("e-mail address already in use", textPlainHeaders, HttpStatus.BAD_REQUEST)) + .orElseGet(() -> { + User user = userService + .createUser(managedUserVM.getLogin(), managedUserVM.getPassword(), + managedUserVM.getFirstName(), managedUserVM.getLastName(), + managedUserVM.getEmail().toLowerCase(), managedUserVM.getImageUrl(), managedUserVM.getLangKey()); + + mailService.sendActivationEmail(user); + return new ResponseEntity<>(HttpStatus.CREATED); + }) + ); + } + + /** + * GET /activate : activate the registered user. + * + * @param key the activation key + * @return the ResponseEntity with status 200 (OK) and the activated user in body, or status 500 (Internal Server Error) if the user couldn't be activated + */ + @GetMapping("/activate") + @Timed + public ResponseEntity activateAccount(@RequestParam(value = "key") String key) { + return userService.activateRegistration(key) + .map(user -> new ResponseEntity(HttpStatus.OK)) + .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + + /** + * GET /authenticate : check if the user is authenticated, and return its login. + * + * @param request the HTTP request + * @return the login if the user is authenticated + */ + @GetMapping("/authenticate") + @Timed + public String isAuthenticated(HttpServletRequest request) { + log.debug("REST request to check if the current user is authenticated"); + return request.getRemoteUser(); + } + + /** + * GET /account : get the current user. + * + * @return the ResponseEntity with status 200 (OK) and the current user in body, or status 500 (Internal Server Error) if the user couldn't be returned + */ + @GetMapping("/account") + @Timed + public ResponseEntity getAccount() { + return Optional.ofNullable(userService.getUserWithAuthorities()) + .map(user -> new ResponseEntity<>(new UserDTO(user), HttpStatus.OK)) + .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + + /** + * POST /account : update the current user information. + * + * @param userDTO the current user information + * @return the ResponseEntity with status 200 (OK), or status 400 (Bad Request) or 500 (Internal Server Error) if the user couldn't be updated + */ + @PostMapping("/account") + @Timed + public ResponseEntity saveAccount(@Valid @RequestBody UserDTO userDTO) { + Optional existingUser = userRepository.findOneByEmail(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.get().getLogin().equalsIgnoreCase(userDTO.getLogin()))) { + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert("user-management", "emailexists", "Email already in use")).body(null); + } + return userRepository + .findOneByLogin(SecurityUtils.getCurrentUserLogin()) + .map(u -> { + userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(), + userDTO.getLangKey()); + return new ResponseEntity(HttpStatus.OK); + }) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + + /** + * POST /account/change_password : changes the current user's password + * + * @param password the new password + * @return the ResponseEntity with status 200 (OK), or status 400 (Bad Request) if the new password is not strong enough + */ + @PostMapping(path = "/account/change_password", + produces = MediaType.TEXT_PLAIN_VALUE) + @Timed + public ResponseEntity changePassword(@RequestBody String password) { + if (!checkPasswordLength(password)) { + return new ResponseEntity<>("Incorrect password", HttpStatus.BAD_REQUEST); + } + userService.changePassword(password); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * POST /account/reset_password/init : Send an e-mail to reset the password of the user + * + * @param mail the mail of the user + * @return the ResponseEntity with status 200 (OK) if the e-mail was sent, or status 400 (Bad Request) if the e-mail address is not registered + */ + @PostMapping(path = "/account/reset_password/init", + produces = MediaType.TEXT_PLAIN_VALUE) + @Timed + public ResponseEntity requestPasswordReset(@RequestBody String mail) { + return userService.requestPasswordReset(mail) + .map(user -> { + mailService.sendPasswordResetMail(user); + return new ResponseEntity<>("e-mail was sent", HttpStatus.OK); + }).orElse(new ResponseEntity<>("e-mail address not registered", HttpStatus.BAD_REQUEST)); + } + + /** + * POST /account/reset_password/finish : Finish to reset the password of the user + * + * @param keyAndPassword the generated key and the new password + * @return the ResponseEntity with status 200 (OK) if the password has been reset, + * or status 400 (Bad Request) or 500 (Internal Server Error) if the password could not be reset + */ + @PostMapping(path = "/account/reset_password/finish", + produces = MediaType.TEXT_PLAIN_VALUE) + @Timed + public ResponseEntity finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) { + if (!checkPasswordLength(keyAndPassword.getNewPassword())) { + return new ResponseEntity<>("Incorrect password", HttpStatus.BAD_REQUEST); + } + return userService.completePasswordReset(keyAndPassword.getNewPassword(), keyAndPassword.getKey()) + .map(user -> new ResponseEntity(HttpStatus.OK)) + .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + + private boolean checkPasswordLength(String password) { + return !StringUtils.isEmpty(password) && + password.length() >= ManagedUserVM.PASSWORD_MIN_LENGTH && + password.length() <= ManagedUserVM.PASSWORD_MAX_LENGTH; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/AuditResource.java b/jhipster/src/main/java/com/baeldung/web/rest/AuditResource.java new file mode 100644 index 0000000000..3101d134ed --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/AuditResource.java @@ -0,0 +1,76 @@ +package com.baeldung.web.rest; + +import com.baeldung.service.AuditEventService; +import com.baeldung.web.rest.util.PaginationUtil; + +import io.github.jhipster.web.util.ResponseUtil; +import io.swagger.annotations.ApiParam; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URISyntaxException; +import java.time.LocalDate; +import java.util.List; + +/** + * REST controller for getting the audit events. + */ +@RestController +@RequestMapping("/management/audits") +public class AuditResource { + + private final AuditEventService auditEventService; + + public AuditResource(AuditEventService auditEventService) { + this.auditEventService = auditEventService; + } + + /** + * GET /audits : get a page of AuditEvents. + * + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and the list of AuditEvents in body + */ + @GetMapping + public ResponseEntity> getAll(@ApiParam Pageable pageable) { + Page page = auditEventService.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/management/audits"); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + /** + * GET /audits : get a page of AuditEvents between the fromDate and toDate. + * + * @param fromDate the start of the time period of AuditEvents to get + * @param toDate the end of the time period of AuditEvents to get + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and the list of AuditEvents in body + */ + + @GetMapping(params = {"fromDate", "toDate"}) + public ResponseEntity> getByDates( + @RequestParam(value = "fromDate") LocalDate fromDate, + @RequestParam(value = "toDate") LocalDate toDate, + @ApiParam Pageable pageable) { + + Page page = auditEventService.findByDates(fromDate.atTime(0, 0), toDate.atTime(23, 59), pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/management/audits"); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + /** + * GET /audits/:id : get an AuditEvent by id. + * + * @param id the id of the entity to get + * @return the ResponseEntity with status 200 (OK) and the AuditEvent in body, or status 404 (Not Found) + */ + @GetMapping("/{id:.+}") + public ResponseEntity get(@PathVariable Long id) { + return ResponseUtil.wrapOrNotFound(auditEventService.find(id)); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/CommentResource.java b/jhipster/src/main/java/com/baeldung/web/rest/CommentResource.java new file mode 100644 index 0000000000..3e3f13b5e9 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/CommentResource.java @@ -0,0 +1,129 @@ +package com.baeldung.web.rest; + +import com.codahale.metrics.annotation.Timed; +import com.baeldung.domain.Comment; + +import com.baeldung.repository.CommentRepository; +import com.baeldung.web.rest.util.HeaderUtil; +import com.baeldung.web.rest.util.PaginationUtil; +import io.swagger.annotations.ApiParam; +import io.github.jhipster.web.util.ResponseUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; + +/** + * REST controller for managing Comment. + */ +@RestController +@RequestMapping("/api") +public class CommentResource { + + private final Logger log = LoggerFactory.getLogger(CommentResource.class); + + private static final String ENTITY_NAME = "comment"; + + private final CommentRepository commentRepository; + + public CommentResource(CommentRepository commentRepository) { + this.commentRepository = commentRepository; + } + + /** + * POST /comments : Create a new comment. + * + * @param comment the comment to create + * @return the ResponseEntity with status 201 (Created) and with body the new comment, or with status 400 (Bad Request) if the comment has already an ID + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("/comments") + @Timed + public ResponseEntity createComment(@Valid @RequestBody Comment comment) throws URISyntaxException { + log.debug("REST request to save Comment : {}", comment); + if (comment.getId() != null) { + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "idexists", "A new comment cannot already have an ID")).body(null); + } + Comment result = commentRepository.save(comment); + return ResponseEntity.created(new URI("/api/comments/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * PUT /comments : Updates an existing comment. + * + * @param comment the comment to update + * @return the ResponseEntity with status 200 (OK) and with body the updated comment, + * or with status 400 (Bad Request) if the comment is not valid, + * or with status 500 (Internal Server Error) if the comment couldnt be updated + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PutMapping("/comments") + @Timed + public ResponseEntity updateComment(@Valid @RequestBody Comment comment) throws URISyntaxException { + log.debug("REST request to update Comment : {}", comment); + if (comment.getId() == null) { + return createComment(comment); + } + Comment result = commentRepository.save(comment); + return ResponseEntity.ok() + .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, comment.getId().toString())) + .body(result); + } + + /** + * GET /comments : get all the comments. + * + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and the list of comments in body + * @throws URISyntaxException if there is an error to generate the pagination HTTP headers + */ + @GetMapping("/comments") + @Timed + public ResponseEntity> getAllComments(@ApiParam Pageable pageable) { + log.debug("REST request to get a page of Comments"); + Page page = commentRepository.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/comments"); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + /** + * GET /comments/:id : get the "id" comment. + * + * @param id the id of the comment to retrieve + * @return the ResponseEntity with status 200 (OK) and with body the comment, or with status 404 (Not Found) + */ + @GetMapping("/comments/{id}") + @Timed + public ResponseEntity getComment(@PathVariable Long id) { + log.debug("REST request to get Comment : {}", id); + Comment comment = commentRepository.findOne(id); + return ResponseUtil.wrapOrNotFound(Optional.ofNullable(comment)); + } + + /** + * DELETE /comments/:id : delete the "id" comment. + * + * @param id the id of the comment to delete + * @return the ResponseEntity with status 200 (OK) + */ + @DeleteMapping("/comments/{id}") + @Timed + public ResponseEntity deleteComment(@PathVariable Long id) { + log.debug("REST request to delete Comment : {}", id); + commentRepository.delete(id); + return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build(); + } + +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/JWTToken.java b/jhipster/src/main/java/com/baeldung/web/rest/JWTToken.java new file mode 100644 index 0000000000..c0804851f3 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/JWTToken.java @@ -0,0 +1,24 @@ +package com.baeldung.web.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Object to return as body in JWT Authentication. + */ +public class JWTToken { + + private String idToken; + + public JWTToken(String idToken) { + this.idToken = idToken; + } + + @JsonProperty("id_token") + public String getIdToken() { + return idToken; + } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/LogsResource.java b/jhipster/src/main/java/com/baeldung/web/rest/LogsResource.java new file mode 100644 index 0000000000..3d556e2609 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/LogsResource.java @@ -0,0 +1,39 @@ +package com.baeldung.web.rest; + +import com.baeldung.web.rest.vm.LoggerVM; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import com.codahale.metrics.annotation.Timed; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Controller for view and managing Log Level at runtime. + */ +@RestController +@RequestMapping("/management") +public class LogsResource { + + @GetMapping("/logs") + @Timed + public List getList() { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + return context.getLoggerList() + .stream() + .map(LoggerVM::new) + .collect(Collectors.toList()); + } + + @PutMapping("/logs") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Timed + public void changeLevel(@RequestBody LoggerVM jsonLogger) { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + context.getLogger(jsonLogger.getName()).setLevel(Level.valueOf(jsonLogger.getLevel())); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/PostResource.java b/jhipster/src/main/java/com/baeldung/web/rest/PostResource.java new file mode 100644 index 0000000000..7505e1fa46 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/PostResource.java @@ -0,0 +1,129 @@ +package com.baeldung.web.rest; + +import com.codahale.metrics.annotation.Timed; +import com.baeldung.domain.Post; + +import com.baeldung.repository.PostRepository; +import com.baeldung.web.rest.util.HeaderUtil; +import com.baeldung.web.rest.util.PaginationUtil; +import io.swagger.annotations.ApiParam; +import io.github.jhipster.web.util.ResponseUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; + +/** + * REST controller for managing Post. + */ +@RestController +@RequestMapping("/api") +public class PostResource { + + private final Logger log = LoggerFactory.getLogger(PostResource.class); + + private static final String ENTITY_NAME = "post"; + + private final PostRepository postRepository; + + public PostResource(PostRepository postRepository) { + this.postRepository = postRepository; + } + + /** + * POST /posts : Create a new post. + * + * @param post the post to create + * @return the ResponseEntity with status 201 (Created) and with body the new post, or with status 400 (Bad Request) if the post has already an ID + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("/posts") + @Timed + public ResponseEntity createPost(@Valid @RequestBody Post post) throws URISyntaxException { + log.debug("REST request to save Post : {}", post); + if (post.getId() != null) { + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "idexists", "A new post cannot already have an ID")).body(null); + } + Post result = postRepository.save(post); + return ResponseEntity.created(new URI("/api/posts/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * PUT /posts : Updates an existing post. + * + * @param post the post to update + * @return the ResponseEntity with status 200 (OK) and with body the updated post, + * or with status 400 (Bad Request) if the post is not valid, + * or with status 500 (Internal Server Error) if the post couldnt be updated + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PutMapping("/posts") + @Timed + public ResponseEntity updatePost(@Valid @RequestBody Post post) throws URISyntaxException { + log.debug("REST request to update Post : {}", post); + if (post.getId() == null) { + return createPost(post); + } + Post result = postRepository.save(post); + return ResponseEntity.ok() + .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, post.getId().toString())) + .body(result); + } + + /** + * GET /posts : get all the posts. + * + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and the list of posts in body + * @throws URISyntaxException if there is an error to generate the pagination HTTP headers + */ + @GetMapping("/posts") + @Timed + public ResponseEntity> getAllPosts(@ApiParam Pageable pageable) { + log.debug("REST request to get a page of Posts"); + Page page = postRepository.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/posts"); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + /** + * GET /posts/:id : get the "id" post. + * + * @param id the id of the post to retrieve + * @return the ResponseEntity with status 200 (OK) and with body the post, or with status 404 (Not Found) + */ + @GetMapping("/posts/{id}") + @Timed + public ResponseEntity getPost(@PathVariable Long id) { + log.debug("REST request to get Post : {}", id); + Post post = postRepository.findOne(id); + return ResponseUtil.wrapOrNotFound(Optional.ofNullable(post)); + } + + /** + * DELETE /posts/:id : delete the "id" post. + * + * @param id the id of the post to delete + * @return the ResponseEntity with status 200 (OK) + */ + @DeleteMapping("/posts/{id}") + @Timed + public ResponseEntity deletePost(@PathVariable Long id) { + log.debug("REST request to delete Post : {}", id); + postRepository.delete(id); + return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build(); + } + +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/ProfileInfoResource.java b/jhipster/src/main/java/com/baeldung/web/rest/ProfileInfoResource.java new file mode 100644 index 0000000000..b5e26c8281 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/ProfileInfoResource.java @@ -0,0 +1,69 @@ +package com.baeldung.web.rest; + +import com.baeldung.config.DefaultProfileUtil; + +import io.github.jhipster.config.JHipsterProperties; + +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Resource to return information about the currently running Spring profiles. + */ +@RestController +@RequestMapping("/api") +public class ProfileInfoResource { + + private final Environment env; + + private final JHipsterProperties jHipsterProperties; + + public ProfileInfoResource(Environment env, JHipsterProperties jHipsterProperties) { + this.env = env; + this.jHipsterProperties = jHipsterProperties; + } + + @GetMapping("/profile-info") + public ProfileInfoVM getActiveProfiles() { + String[] activeProfiles = DefaultProfileUtil.getActiveProfiles(env); + return new ProfileInfoVM(activeProfiles, getRibbonEnv(activeProfiles)); + } + + private String getRibbonEnv(String[] activeProfiles) { + String[] displayOnActiveProfiles = jHipsterProperties.getRibbon().getDisplayOnActiveProfiles(); + if (displayOnActiveProfiles == null) { + return null; + } + List ribbonProfiles = new ArrayList<>(Arrays.asList(displayOnActiveProfiles)); + List springBootProfiles = Arrays.asList(activeProfiles); + ribbonProfiles.retainAll(springBootProfiles); + if (!ribbonProfiles.isEmpty()) { + return ribbonProfiles.get(0); + } + return null; + } + + class ProfileInfoVM { + + private String[] activeProfiles; + + private String ribbonEnv; + + ProfileInfoVM(String[] activeProfiles, String ribbonEnv) { + this.activeProfiles = activeProfiles; + this.ribbonEnv = ribbonEnv; + } + + public String[] getActiveProfiles() { + return activeProfiles; + } + + public String getRibbonEnv() { + return ribbonEnv; + } + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/UserJWTController.java b/jhipster/src/main/java/com/baeldung/web/rest/UserJWTController.java new file mode 100644 index 0000000000..02b77c51a6 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/UserJWTController.java @@ -0,0 +1,60 @@ +package com.baeldung.web.rest; + +import com.baeldung.security.jwt.JWTConfigurer; +import com.baeldung.security.jwt.TokenProvider; +import com.baeldung.web.rest.vm.LoginVM; + +import java.util.Collections; + +import com.codahale.metrics.annotation.Timed; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +@RestController +@RequestMapping("/api") +public class UserJWTController { + + private final Logger log = LoggerFactory.getLogger(UserJWTController.class); + + private final TokenProvider tokenProvider; + + private final AuthenticationManager authenticationManager; + + public UserJWTController(TokenProvider tokenProvider, AuthenticationManager authenticationManager) { + this.tokenProvider = tokenProvider; + this.authenticationManager = authenticationManager; + } + + @PostMapping("/authenticate") + @Timed + public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, HttpServletResponse response) { + + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); + + try { + Authentication authentication = this.authenticationManager.authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + boolean rememberMe = (loginVM.isRememberMe() == null) ? false : loginVM.isRememberMe(); + String jwt = tokenProvider.createToken(authentication, rememberMe); + response.addHeader(JWTConfigurer.AUTHORIZATION_HEADER, "Bearer " + jwt); + return ResponseEntity.ok(new JWTToken(jwt)); + } catch (AuthenticationException ae) { + log.trace("Authentication exception trace: {}", ae); + return new ResponseEntity<>(Collections.singletonMap("AuthenticationException", + ae.getLocalizedMessage()), HttpStatus.UNAUTHORIZED); + } + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/UserResource.java b/jhipster/src/main/java/com/baeldung/web/rest/UserResource.java new file mode 100644 index 0000000000..296d3e30ba --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/UserResource.java @@ -0,0 +1,187 @@ +package com.baeldung.web.rest; + +import com.baeldung.config.Constants; +import com.codahale.metrics.annotation.Timed; +import com.baeldung.domain.User; +import com.baeldung.repository.UserRepository; +import com.baeldung.security.AuthoritiesConstants; +import com.baeldung.service.MailService; +import com.baeldung.service.UserService; +import com.baeldung.service.dto.UserDTO; +import com.baeldung.web.rest.vm.ManagedUserVM; +import com.baeldung.web.rest.util.HeaderUtil; +import com.baeldung.web.rest.util.PaginationUtil; +import io.github.jhipster.web.util.ResponseUtil; +import io.swagger.annotations.ApiParam; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; + +/** + * REST controller for managing users. + * + *

This class accesses the User entity, and needs to fetch its collection of authorities.

+ *

+ * For a normal use-case, it would be better to have an eager relationship between User and Authority, + * and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join + * which would be good for performance. + *

+ *

+ * We use a View Model and a DTO for 3 reasons: + *

    + *
  • We want to keep a lazy association between the user and the authorities, because people will + * quite often do relationships with the user, and we don't want them to get the authorities all + * the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users' + * application because of this use-case.
  • + *
  • Not having an outer join causes n+1 requests to the database. This is not a real issue as + * we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests, + * but then all authorities come from the cache, so in fact it's much better than doing an outer join + * (which will get lots of data from the database, for each HTTP call).
  • + *
  • As this manages users, for security reasons, we'd rather have a DTO layer.
  • + *
+ *

Another option would be to have a specific JPA entity graph to handle this case.

+ */ +@RestController +@RequestMapping("/api") +public class UserResource { + + private final Logger log = LoggerFactory.getLogger(UserResource.class); + + private static final String ENTITY_NAME = "userManagement"; + + private final UserRepository userRepository; + + private final MailService mailService; + + private final UserService userService; + + public UserResource(UserRepository userRepository, MailService mailService, + UserService userService) { + + this.userRepository = userRepository; + this.mailService = mailService; + this.userService = userService; + } + + /** + * POST /users : Creates a new user. + *

+ * Creates a new user if the login and email are not already used, and sends an + * mail with an activation link. + * The user needs to be activated on creation. + *

+ * + * @param managedUserVM the user to create + * @return the ResponseEntity with status 201 (Created) and with body the new user, or with status 400 (Bad Request) if the login or email is already in use + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("/users") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public ResponseEntity createUser(@RequestBody ManagedUserVM managedUserVM) throws URISyntaxException { + log.debug("REST request to save User : {}", managedUserVM); + + if (managedUserVM.getId() != null) { + return ResponseEntity.badRequest() + .headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "idexists", "A new user cannot already have an ID")) + .body(null); + // Lowercase the user login before comparing with database + } else if (userRepository.findOneByLogin(managedUserVM.getLogin().toLowerCase()).isPresent()) { + return ResponseEntity.badRequest() + .headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "userexists", "Login already in use")) + .body(null); + } else if (userRepository.findOneByEmail(managedUserVM.getEmail()).isPresent()) { + return ResponseEntity.badRequest() + .headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "emailexists", "Email already in use")) + .body(null); + } else { + User newUser = userService.createUser(managedUserVM); + mailService.sendCreationEmail(newUser); + return ResponseEntity.created(new URI("/api/users/" + newUser.getLogin())) + .headers(HeaderUtil.createAlert( "userManagement.created", newUser.getLogin())) + .body(newUser); + } + } + + /** + * PUT /users : Updates an existing User. + * + * @param managedUserVM the user to update + * @return the ResponseEntity with status 200 (OK) and with body the updated user, + * or with status 400 (Bad Request) if the login or email is already in use, + * or with status 500 (Internal Server Error) if the user couldn't be updated + */ + @PutMapping("/users") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public ResponseEntity updateUser(@RequestBody ManagedUserVM managedUserVM) { + log.debug("REST request to update User : {}", managedUserVM); + Optional existingUser = userRepository.findOneByEmail(managedUserVM.getEmail()); + if (existingUser.isPresent() && (!existingUser.get().getId().equals(managedUserVM.getId()))) { + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "emailexists", "E-mail already in use")).body(null); + } + existingUser = userRepository.findOneByLogin(managedUserVM.getLogin().toLowerCase()); + if (existingUser.isPresent() && (!existingUser.get().getId().equals(managedUserVM.getId()))) { + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "userexists", "Login already in use")).body(null); + } + Optional updatedUser = userService.updateUser(managedUserVM); + + return ResponseUtil.wrapOrNotFound(updatedUser, + HeaderUtil.createAlert("userManagement.updated", managedUserVM.getLogin())); + } + + /** + * GET /users : get all users. + * + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and with body all users + */ + @GetMapping("/users") + @Timed + public ResponseEntity> getAllUsers(@ApiParam Pageable pageable) { + final Page page = userService.getAllManagedUsers(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/users"); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + /** + * GET /users/:login : get the "login" user. + * + * @param login the login of the user to find + * @return the ResponseEntity with status 200 (OK) and with body the "login" user, or with status 404 (Not Found) + */ + @GetMapping("/users/{login:" + Constants.LOGIN_REGEX + "}") + @Timed + public ResponseEntity getUser(@PathVariable String login) { + log.debug("REST request to get User : {}", login); + return ResponseUtil.wrapOrNotFound( + userService.getUserWithAuthoritiesByLogin(login) + .map(UserDTO::new)); + } + + /** + * DELETE /users/:login : delete the "login" User. + * + * @param login the login of the user to delete + * @return the ResponseEntity with status 200 (OK) + */ + @DeleteMapping("/users/{login:" + Constants.LOGIN_REGEX + "}") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public ResponseEntity deleteUser(@PathVariable String login) { + log.debug("REST request to delete User: {}", login); + userService.deleteUser(login); + return ResponseEntity.ok().headers(HeaderUtil.createAlert( "userManagement.deleted", login)).build(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/errors/CustomParameterizedException.java b/jhipster/src/main/java/com/baeldung/web/rest/errors/CustomParameterizedException.java new file mode 100644 index 0000000000..ab7754476b --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/errors/CustomParameterizedException.java @@ -0,0 +1,34 @@ +package com.baeldung.web.rest.errors; + +/** + * Custom, parameterized exception, which can be translated on the client side. + * For example: + * + *
+ * throw new CustomParameterizedException("myCustomError", "hello", "world");
+ * 
+ * + * Can be translated with: + * + *
+ * "error.myCustomError" :  "The server says {{params[0]}} to {{params[1]}}"
+ * 
+ */ +public class CustomParameterizedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String message; + private final String[] params; + + public CustomParameterizedException(String message, String... params) { + super(message); + this.message = message; + this.params = params; + } + + public ParameterizedErrorVM getErrorVM() { + return new ParameterizedErrorVM(message, params); + } + +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorConstants.java b/jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorConstants.java new file mode 100644 index 0000000000..69f21ed96c --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorConstants.java @@ -0,0 +1,14 @@ +package com.baeldung.web.rest.errors; + +public final class ErrorConstants { + + public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure"; + public static final String ERR_ACCESS_DENIED = "error.accessDenied"; + public static final String ERR_VALIDATION = "error.validation"; + public static final String ERR_METHOD_NOT_SUPPORTED = "error.methodNotSupported"; + public static final String ERR_INTERNAL_SERVER_ERROR = "error.internalServerError"; + + private ErrorConstants() { + } + +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorVM.java b/jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorVM.java new file mode 100644 index 0000000000..22fb066135 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/errors/ErrorVM.java @@ -0,0 +1,52 @@ +package com.baeldung.web.rest.errors; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * View Model for transferring error message with a list of field errors. + */ +public class ErrorVM implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String message; + private final String description; + + private List fieldErrors; + + public ErrorVM(String message) { + this(message, null); + } + + public ErrorVM(String message, String description) { + this.message = message; + this.description = description; + } + + public ErrorVM(String message, String description, List fieldErrors) { + this.message = message; + this.description = description; + this.fieldErrors = fieldErrors; + } + + public void add(String objectName, String field, String message) { + if (fieldErrors == null) { + fieldErrors = new ArrayList<>(); + } + fieldErrors.add(new FieldErrorVM(objectName, field, message)); + } + + public String getMessage() { + return message; + } + + public String getDescription() { + return description; + } + + public List getFieldErrors() { + return fieldErrors; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/errors/ExceptionTranslator.java b/jhipster/src/main/java/com/baeldung/web/rest/errors/ExceptionTranslator.java new file mode 100644 index 0000000000..51925bfa61 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/errors/ExceptionTranslator.java @@ -0,0 +1,85 @@ +package com.baeldung.web.rest.errors; + +import java.util.List; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.ResponseEntity.BodyBuilder; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.*; + +/** + * Controller advice to translate the server side exceptions to client-friendly json structures. + */ +@ControllerAdvice +public class ExceptionTranslator { + + @ExceptionHandler(ConcurrencyFailureException.class) + @ResponseStatus(HttpStatus.CONFLICT) + @ResponseBody + public ErrorVM processConcurrencyError(ConcurrencyFailureException ex) { + return new ErrorVM(ErrorConstants.ERR_CONCURRENCY_FAILURE); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ErrorVM processValidationError(MethodArgumentNotValidException ex) { + BindingResult result = ex.getBindingResult(); + List fieldErrors = result.getFieldErrors(); + + return processFieldErrors(fieldErrors); + } + + @ExceptionHandler(CustomParameterizedException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ParameterizedErrorVM processParameterizedValidationError(CustomParameterizedException ex) { + return ex.getErrorVM(); + } + + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + @ResponseBody + public ErrorVM processAccessDeniedException(AccessDeniedException e) { + return new ErrorVM(ErrorConstants.ERR_ACCESS_DENIED, e.getMessage()); + } + + private ErrorVM processFieldErrors(List fieldErrors) { + ErrorVM dto = new ErrorVM(ErrorConstants.ERR_VALIDATION); + + for (FieldError fieldError : fieldErrors) { + dto.add(fieldError.getObjectName(), fieldError.getField(), fieldError.getCode()); + } + + return dto; + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseBody + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public ErrorVM processMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) { + return new ErrorVM(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, exception.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity processRuntimeException(Exception ex) { + BodyBuilder builder; + ErrorVM errorVM; + ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); + if (responseStatus != null) { + builder = ResponseEntity.status(responseStatus.value()); + errorVM = new ErrorVM("error." + responseStatus.value().value(), responseStatus.reason()); + } else { + builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR); + errorVM = new ErrorVM(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, "Internal server error"); + } + return builder.body(errorVM); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/errors/FieldErrorVM.java b/jhipster/src/main/java/com/baeldung/web/rest/errors/FieldErrorVM.java new file mode 100644 index 0000000000..19ab640ec7 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/errors/FieldErrorVM.java @@ -0,0 +1,33 @@ +package com.baeldung.web.rest.errors; + +import java.io.Serializable; + +public class FieldErrorVM implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String objectName; + + private final String field; + + private final String message; + + public FieldErrorVM(String dto, String field, String message) { + this.objectName = dto; + this.field = field; + this.message = message; + } + + public String getObjectName() { + return objectName; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } + +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/errors/ParameterizedErrorVM.java b/jhipster/src/main/java/com/baeldung/web/rest/errors/ParameterizedErrorVM.java new file mode 100644 index 0000000000..18500c51af --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/errors/ParameterizedErrorVM.java @@ -0,0 +1,27 @@ +package com.baeldung.web.rest.errors; + +import java.io.Serializable; + +/** + * View Model for sending a parameterized error message. + */ +public class ParameterizedErrorVM implements Serializable { + + private static final long serialVersionUID = 1L; + private final String message; + private final String[] params; + + public ParameterizedErrorVM(String message, String... params) { + this.message = message; + this.params = params; + } + + public String getMessage() { + return message; + } + + public String[] getParams() { + return params; + } + +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/package-info.java b/jhipster/src/main/java/com/baeldung/web/rest/package-info.java new file mode 100644 index 0000000000..0a74f6e90c --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring MVC REST controllers. + */ +package com.baeldung.web.rest; diff --git a/jhipster/src/main/java/com/baeldung/web/rest/util/HeaderUtil.java b/jhipster/src/main/java/com/baeldung/web/rest/util/HeaderUtil.java new file mode 100644 index 0000000000..7c643e9323 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/util/HeaderUtil.java @@ -0,0 +1,45 @@ +package com.baeldung.web.rest.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; + +/** + * Utility class for HTTP headers creation. + */ +public final class HeaderUtil { + + private static final Logger log = LoggerFactory.getLogger(HeaderUtil.class); + + private static final String APPLICATION_NAME = "baeldungApp"; + + private HeaderUtil() { + } + + public static HttpHeaders createAlert(String message, String param) { + HttpHeaders headers = new HttpHeaders(); + headers.add("X-baeldungApp-alert", message); + headers.add("X-baeldungApp-params", param); + return headers; + } + + public static HttpHeaders createEntityCreationAlert(String entityName, String param) { + return createAlert(APPLICATION_NAME + "." + entityName + ".created", param); + } + + public static HttpHeaders createEntityUpdateAlert(String entityName, String param) { + return createAlert(APPLICATION_NAME + "." + entityName + ".updated", param); + } + + public static HttpHeaders createEntityDeletionAlert(String entityName, String param) { + return createAlert(APPLICATION_NAME + "." + entityName + ".deleted", param); + } + + public static HttpHeaders createFailureAlert(String entityName, String errorKey, String defaultMessage) { + log.error("Entity creation failed, {}", defaultMessage); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-baeldungApp-error", "error." + errorKey); + headers.add("X-baeldungApp-params", entityName); + return headers; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/util/PaginationUtil.java b/jhipster/src/main/java/com/baeldung/web/rest/util/PaginationUtil.java new file mode 100644 index 0000000000..affcb7842c --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/util/PaginationUtil.java @@ -0,0 +1,47 @@ +package com.baeldung.web.rest.util; + +import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URISyntaxException; + +/** + * Utility class for handling pagination. + * + *

+ * Pagination uses the same principles as the Github API, + * and follow RFC 5988 (Link header). + */ +public final class PaginationUtil { + + private PaginationUtil() { + } + + public static HttpHeaders generatePaginationHttpHeaders(Page page, String baseUrl) { + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Total-Count", "" + Long.toString(page.getTotalElements())); + String link = ""; + if ((page.getNumber() + 1) < page.getTotalPages()) { + link = "<" + generateUri(baseUrl, page.getNumber() + 1, page.getSize()) + ">; rel=\"next\","; + } + // prev link + if ((page.getNumber()) > 0) { + link += "<" + generateUri(baseUrl, page.getNumber() - 1, page.getSize()) + ">; rel=\"prev\","; + } + // last and first link + int lastPage = 0; + if (page.getTotalPages() > 0) { + lastPage = page.getTotalPages() - 1; + } + link += "<" + generateUri(baseUrl, lastPage, page.getSize()) + ">; rel=\"last\","; + link += "<" + generateUri(baseUrl, 0, page.getSize()) + ">; rel=\"first\""; + headers.add(HttpHeaders.LINK, link); + return headers; + } + + private static String generateUri(String baseUrl, int page, int size) { + return UriComponentsBuilder.fromUriString(baseUrl).queryParam("page", page).queryParam("size", size).toUriString(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/vm/KeyAndPasswordVM.java b/jhipster/src/main/java/com/baeldung/web/rest/vm/KeyAndPasswordVM.java new file mode 100644 index 0000000000..94465f309f --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/vm/KeyAndPasswordVM.java @@ -0,0 +1,27 @@ +package com.baeldung.web.rest.vm; + +/** + * View Model object for storing the user's key and password. + */ +public class KeyAndPasswordVM { + + private String key; + + private String newPassword; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/vm/LoggerVM.java b/jhipster/src/main/java/com/baeldung/web/rest/vm/LoggerVM.java new file mode 100644 index 0000000000..56f77aec85 --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/vm/LoggerVM.java @@ -0,0 +1,48 @@ +package com.baeldung.web.rest.vm; + +import ch.qos.logback.classic.Logger; +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * View Model object for storing a Logback logger. + */ +public class LoggerVM { + + private String name; + + private String level; + + public LoggerVM(Logger logger) { + this.name = logger.getName(); + this.level = logger.getEffectiveLevel().toString(); + } + + @JsonCreator + public LoggerVM() { + // Empty public constructor used by Jackson. + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + @Override + public String toString() { + return "LoggerVM{" + + "name='" + name + '\'' + + ", level='" + level + '\'' + + '}'; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/vm/LoginVM.java b/jhipster/src/main/java/com/baeldung/web/rest/vm/LoginVM.java new file mode 100644 index 0000000000..a910d0d59c --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/vm/LoginVM.java @@ -0,0 +1,55 @@ +package com.baeldung.web.rest.vm; + +import com.baeldung.config.Constants; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * View Model object for storing a user's credentials. + */ +public class LoginVM { + + @Pattern(regexp = Constants.LOGIN_REGEX) + @NotNull + @Size(min = 1, max = 50) + private String username; + + @NotNull + @Size(min = ManagedUserVM.PASSWORD_MIN_LENGTH, max = ManagedUserVM.PASSWORD_MAX_LENGTH) + private String password; + + private Boolean rememberMe; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(Boolean rememberMe) { + this.rememberMe = rememberMe; + } + + @Override + public String toString() { + return "LoginVM{" + + "username='" + username + '\'' + + ", rememberMe=" + rememberMe + + '}'; + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/vm/ManagedUserVM.java b/jhipster/src/main/java/com/baeldung/web/rest/vm/ManagedUserVM.java new file mode 100644 index 0000000000..0899f0d0aa --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/vm/ManagedUserVM.java @@ -0,0 +1,45 @@ +package com.baeldung.web.rest.vm; + +import com.baeldung.service.dto.UserDTO; +import javax.validation.constraints.Size; + +import java.time.ZonedDateTime; +import java.util.Set; + +/** + * View Model extending the UserDTO, which is meant to be used in the user management UI. + */ +public class ManagedUserVM extends UserDTO { + + public static final int PASSWORD_MIN_LENGTH = 4; + + public static final int PASSWORD_MAX_LENGTH = 100; + + @Size(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH) + private String password; + + public ManagedUserVM() { + // Empty constructor needed for Jackson. + } + + public ManagedUserVM(Long id, String login, String password, String firstName, String lastName, + String email, boolean activated, String imageUrl, String langKey, + String createdBy, ZonedDateTime createdDate, String lastModifiedBy, ZonedDateTime lastModifiedDate, + Set authorities) { + + super(id, login, firstName, lastName, email, activated, imageUrl, langKey, + createdBy, createdDate, lastModifiedBy, lastModifiedDate, authorities); + + this.password = password; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "ManagedUserVM{" + + "} " + super.toString(); + } +} diff --git a/jhipster/src/main/java/com/baeldung/web/rest/vm/package-info.java b/jhipster/src/main/java/com/baeldung/web/rest/vm/package-info.java new file mode 100644 index 0000000000..8d56157b6a --- /dev/null +++ b/jhipster/src/main/java/com/baeldung/web/rest/vm/package-info.java @@ -0,0 +1,4 @@ +/** + * View Models used by Spring MVC REST controllers. + */ +package com.baeldung.web.rest.vm; diff --git a/jhipster/src/main/resources/.h2.server.properties b/jhipster/src/main/resources/.h2.server.properties new file mode 100644 index 0000000000..f8b4429902 --- /dev/null +++ b/jhipster/src/main/resources/.h2.server.properties @@ -0,0 +1,5 @@ +#H2 Server Properties +0=JHipster H2 (Disk)|org.h2.Driver|jdbc\:h2\:file\:./target/h2db/db/baeldung|baeldung +webAllowOthers=true +webPort=8082 +webSSL=false diff --git a/jhipster/src/main/resources/banner.txt b/jhipster/src/main/resources/banner.txt new file mode 100644 index 0000000000..c3d8cf725d --- /dev/null +++ b/jhipster/src/main/resources/banner.txt @@ -0,0 +1,10 @@ + + ${AnsiColor.GREEN} ██╗${AnsiColor.RED} ██╗ ██╗ ████████╗ ███████╗ ██████╗ ████████╗ ████████╗ ███████╗ + ${AnsiColor.GREEN} ██║${AnsiColor.RED} ██║ ██║ ╚══██╔══╝ ██╔═══██╗ ██╔════╝ ╚══██╔══╝ ██╔═════╝ ██╔═══██╗ + ${AnsiColor.GREEN} ██║${AnsiColor.RED} ████████║ ██║ ███████╔╝ ╚█████╗ ██║ ██████╗ ███████╔╝ + ${AnsiColor.GREEN}██╗ ██║${AnsiColor.RED} ██╔═══██║ ██║ ██╔════╝ ╚═══██╗ ██║ ██╔═══╝ ██╔══██║ + ${AnsiColor.GREEN}╚██████╔╝${AnsiColor.RED} ██║ ██║ ████████╗ ██║ ██████╔╝ ██║ ████████╗ ██║ ╚██╗ + ${AnsiColor.GREEN} ╚═════╝ ${AnsiColor.RED} ╚═╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═╝ + +${AnsiColor.BRIGHT_BLUE}:: JHipster 🤓 :: Running Spring Boot ${spring-boot.version} :: +:: http://jhipster.github.io ::${AnsiColor.DEFAULT} diff --git a/jhipster/src/main/resources/config/application-dev.yml b/jhipster/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000000..d75c2549c8 --- /dev/null +++ b/jhipster/src/main/resources/config/application-dev.yml @@ -0,0 +1,130 @@ +# =================================================================== +# Spring Boot configuration for the "dev" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://jhipster.github.io/profiles/ +# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +spring: + profiles: + active: dev + include: swagger + devtools: + restart: + enabled: true + livereload: + enabled: false # we use gulp + BrowserSync for livereload + jackson: + serialization.indent_output: true + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:h2:file:./target/h2db/db/baeldung;DB_CLOSE_DELAY=-1 + username: baeldung + password: + h2: + console: + enabled: false + jpa: + database-platform: io.github.jhipster.domain.util.FixedH2Dialect + database: H2 + show-sql: true + properties: + hibernate.id.new_generator_mappings: true + hibernate.cache.use_second_level_cache: true + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: true + hibernate.cache.region.factory_class: io.github.jhipster.config.jcache.NoDefaultJCacheRegionFactory + mail: + host: localhost + port: 25 + username: + password: + messages: + cache-seconds: 1 + thymeleaf: + cache: false + +liquibase: + contexts: dev + +# =================================================================== +# To enable SSL, generate a certificate using: +# keytool -genkey -alias baeldung -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650 +# +# You can also use Let's Encrypt: +# https://maximilian-boehm.com/hp2121/Create-a-Java-Keystore-JKS-from-Let-s-Encrypt-Certificates.htm +# +# Then, modify the server.ssl properties so your "server" configuration looks like: +# +# server: +# port: 8443 +# ssl: +# key-store: keystore.p12 +# key-store-password: +# keyStoreType: PKCS12 +# keyAlias: baeldung +# =================================================================== +server: + port: 8080 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +jhipster: + http: + version: V_1_1 # To use HTTP/2 you will need SSL support (see above the "server.ssl" configuration) + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 100 # Number of objects in each cache entry + security: + authentication: + jwt: + secret: my-secret-token-to-change-in-production + # Token is valid 24 hours + token-validity-in-seconds: 86400 + token-validity-in-seconds-for-remember-me: 2592000 + mail: # specific JHipster mail property, for standard properties see MailProperties + from: baeldung@localhost + base-url: http://127.0.0.1:8080 + metrics: # DropWizard Metrics configuration, used by MetricsConfiguration + jmx.enabled: true + graphite: # Use the "graphite" Maven profile to have the Graphite dependencies + enabled: false + host: localhost + port: 2003 + prefix: baeldung + prometheus: # Use the "prometheus" Maven profile to have the Prometheus dependencies + enabled: false + endpoint: /prometheusMetrics + logs: # Reports Dropwizard metrics in the logs + enabled: false + reportFrequency: 60 # in seconds + logging: + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + queue-size: 512 + +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://jhipster.github.io/common-application-properties/ +# =================================================================== + +application: diff --git a/jhipster/src/main/resources/config/application-prod.yml b/jhipster/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000000..a46fbd1c73 --- /dev/null +++ b/jhipster/src/main/resources/config/application-prod.yml @@ -0,0 +1,132 @@ +# =================================================================== +# Spring Boot configuration for the "prod" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://jhipster.github.io/profiles/ +# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +spring: + devtools: + restart: + enabled: false + livereload: + enabled: false + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://localhost:3306/baeldung?useUnicode=true&characterEncoding=utf8&useSSL=false + username: root + password: + hikari: + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + jpa: + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + database: MYSQL + show-sql: false + properties: + hibernate.id.new_generator_mappings: true + hibernate.cache.use_second_level_cache: true + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: false + hibernate.cache.region.factory_class: io.github.jhipster.config.jcache.NoDefaultJCacheRegionFactory + mail: + host: localhost + port: 25 + username: + password: + thymeleaf: + cache: true + +liquibase: + contexts: prod + +# =================================================================== +# To enable SSL, generate a certificate using: +# keytool -genkey -alias baeldung -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650 +# +# You can also use Let's Encrypt: +# https://maximilian-boehm.com/hp2121/Create-a-Java-Keystore-JKS-from-Let-s-Encrypt-Certificates.htm +# +# Then, modify the server.ssl properties so your "server" configuration looks like: +# +# server: +# port: 443 +# ssl: +# key-store: keystore.p12 +# key-store-password: +# keyStoreType: PKCS12 +# keyAlias: baeldung +# =================================================================== +server: + port: 8080 + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css, application/javascript, application/json + min-response-size: 1024 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +jhipster: + http: + version: V_1_1 # To use HTTP/2 you will need SSL support (see above the "server.ssl" configuration) + cache: # Used by the CachingHttpHeadersFilter + timeToLiveInDays: 1461 + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 1000 # Number of objects in each cache entry + security: + authentication: + jwt: + secret: e1d4b69d3f953e3fa622121e882e6f459ca20ca4 + # Token is valid 24 hours + token-validity-in-seconds: 86400 + token-validity-in-seconds-for-remember-me: 2592000 + mail: # specific JHipster mail property, for standard properties see MailProperties + from: baeldung@localhost + base-url: http://my-server-url-to-change # Modify according to your server's URL + metrics: # DropWizard Metrics configuration, used by MetricsConfiguration + jmx.enabled: true + graphite: + enabled: false + host: localhost + port: 2003 + prefix: baeldung + prometheus: + enabled: false + endpoint: /prometheusMetrics + logs: # Reports Dropwizard metrics in the logs + enabled: false + reportFrequency: 60 # in seconds + logging: + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + queue-size: 512 + +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://jhipster.github.io/common-application-properties/ +# =================================================================== + +application: diff --git a/jhipster/src/main/resources/config/application.yml b/jhipster/src/main/resources/config/application.yml new file mode 100644 index 0000000000..ed48baf853 --- /dev/null +++ b/jhipster/src/main/resources/config/application.yml @@ -0,0 +1,106 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration will be overriden by the Spring profile you use, +# for example application-dev.yml if you use the "dev" profile. +# +# More information on profiles: https://jhipster.github.io/profiles/ +# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +management: + security: + roles: ADMIN + context-path: /management + health: + mail: + enabled: false # When using the MailService, configure an SMTP server and set this to true +spring: + application: + name: baeldung + profiles: + # The commented value for `active` can be replaced with valid Spring profiles to load. + # Otherwise, it will be filled in by maven when building the WAR file + # Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS` + active: #spring.profiles.active# + jackson: + serialization.write_dates_as_timestamps: false + jpa: + open-in-view: false + hibernate: + ddl-auto: none + naming: + physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + messages: + basename: i18n/messages + mvc: + favicon: + enabled: false + thymeleaf: + mode: XHTML + +security: + basic: + enabled: false + +server: + session: + cookie: + http-only: true + +info: + project: + version: #project.version# + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +jhipster: + async: + core-pool-size: 2 + max-pool-size: 50 + queue-capacity: 10000 + # By default CORS is disabled. Uncomment to enable. + #cors: + #allowed-origins: "*" + #allowed-methods: GET, PUT, POST, DELETE, OPTIONS + #allowed-headers: "*" + #exposed-headers: + #allow-credentials: true + #max-age: 1800 + mail: + from: baeldung@localhost + swagger: + default-include-pattern: /api/.* + title: baeldung API + description: baeldung API documentation + version: 0.0.1 + terms-of-service-url: + contact-name: + contact-url: + contact-email: + license: + license-url: + ribbon: + display-on-active-profiles: dev + +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://jhipster.github.io/common-application-properties/ +# =================================================================== + +application: diff --git a/jhipster/src/main/resources/config/liquibase/authorities.csv b/jhipster/src/main/resources/config/liquibase/authorities.csv new file mode 100644 index 0000000000..af5c6dfa18 --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/authorities.csv @@ -0,0 +1,3 @@ +name +ROLE_ADMIN +ROLE_USER diff --git a/jhipster/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml b/jhipster/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml new file mode 100644 index 0000000000..98ac548808 --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_Post.xml b/jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_Post.xml new file mode 100644 index 0000000000..ce4773262c --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_Post.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_constraints_Post.xml b/jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_constraints_Post.xml new file mode 100644 index 0000000000..2040980371 --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/changelog/20170316223211_added_entity_constraints_Post.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_Comment.xml b/jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_Comment.xml new file mode 100644 index 0000000000..d0b26503e1 --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_Comment.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_constraints_Comment.xml b/jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_constraints_Comment.xml new file mode 100644 index 0000000000..b7670e747c --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/changelog/20170316224021_added_entity_constraints_Comment.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/jhipster/src/main/resources/config/liquibase/master.xml b/jhipster/src/main/resources/config/liquibase/master.xml new file mode 100644 index 0000000000..32eb479989 --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/master.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/jhipster/src/main/resources/config/liquibase/users.csv b/jhipster/src/main/resources/config/liquibase/users.csv new file mode 100644 index 0000000000..b25922b699 --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/users.csv @@ -0,0 +1,5 @@ +id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;system;$2a$10$mE.qmcV0mFU5NcKh73TZx.z4ueI/.bDWbj0T1BYyqP481kGGarKLG;System;System;system@localhost;;true;en;system;system +2;anonymoususer;$2a$10$j8S5d7Sr7.8VTOYNviDPOeWX8KcYILUVJBsYV83Y5NtECayypx9lO;Anonymous;User;anonymous@localhost;;true;en;system;system +3;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system +4;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system diff --git a/jhipster/src/main/resources/config/liquibase/users_authorities.csv b/jhipster/src/main/resources/config/liquibase/users_authorities.csv new file mode 100644 index 0000000000..06c5feeeea --- /dev/null +++ b/jhipster/src/main/resources/config/liquibase/users_authorities.csv @@ -0,0 +1,6 @@ +user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +3;ROLE_ADMIN +3;ROLE_USER +4;ROLE_USER diff --git a/jhipster/src/main/resources/i18n/messages.properties b/jhipster/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000000..1c28002acd --- /dev/null +++ b/jhipster/src/main/resources/i18n/messages.properties @@ -0,0 +1,22 @@ +# Error page +error.title=Your request cannot be processed +error.subtitle=Sorry, an error has occurred. +error.status=Status: +error.message=Message: + +# Activation e-mail +email.activation.title=baeldung account activation +email.activation.greeting=Dear {0} +email.activation.text1=Your baeldung account has been created, please click on the URL below to activate it: +email.activation.text2=Regards, +email.signature=baeldung Team. + +# Creation email +email.creation.text1=Your baeldung account has been created, please click on the URL below to access it: + +# Reset e-mail +email.reset.title=baeldung password reset +email.reset.greeting=Dear {0} +email.reset.text1=For your baeldung account a password reset was requested, please click on the URL below to reset it: +email.reset.text2=Regards, + diff --git a/jhipster/src/main/resources/i18n/messages_en.properties b/jhipster/src/main/resources/i18n/messages_en.properties new file mode 100644 index 0000000000..1c28002acd --- /dev/null +++ b/jhipster/src/main/resources/i18n/messages_en.properties @@ -0,0 +1,22 @@ +# Error page +error.title=Your request cannot be processed +error.subtitle=Sorry, an error has occurred. +error.status=Status: +error.message=Message: + +# Activation e-mail +email.activation.title=baeldung account activation +email.activation.greeting=Dear {0} +email.activation.text1=Your baeldung account has been created, please click on the URL below to activate it: +email.activation.text2=Regards, +email.signature=baeldung Team. + +# Creation email +email.creation.text1=Your baeldung account has been created, please click on the URL below to access it: + +# Reset e-mail +email.reset.title=baeldung password reset +email.reset.greeting=Dear {0} +email.reset.text1=For your baeldung account a password reset was requested, please click on the URL below to reset it: +email.reset.text2=Regards, + diff --git a/jhipster/src/main/resources/logback-spring.xml b/jhipster/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000..3c62a70c31 --- /dev/null +++ b/jhipster/src/main/resources/logback-spring.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + diff --git a/jhipster/src/main/resources/mails/activationEmail.html b/jhipster/src/main/resources/mails/activationEmail.html new file mode 100644 index 0000000000..9fb22a7796 --- /dev/null +++ b/jhipster/src/main/resources/mails/activationEmail.html @@ -0,0 +1,24 @@ + + + + JHipster activation + + + +

+ Dear +

+

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

+

+ Activation Link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/jhipster/src/main/resources/mails/creationEmail.html b/jhipster/src/main/resources/mails/creationEmail.html new file mode 100644 index 0000000000..a59d91da0c --- /dev/null +++ b/jhipster/src/main/resources/mails/creationEmail.html @@ -0,0 +1,24 @@ + + + + JHipster creation + + + +

+ Dear +

+

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

+

+ login +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/jhipster/src/main/resources/mails/passwordResetEmail.html b/jhipster/src/main/resources/mails/passwordResetEmail.html new file mode 100644 index 0000000000..30d5f62c60 --- /dev/null +++ b/jhipster/src/main/resources/mails/passwordResetEmail.html @@ -0,0 +1,24 @@ + + + + JHipster password reset + + + +

+ Dear +

+

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

+

+ Reset Link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/jhipster/src/main/resources/templates/error.html b/jhipster/src/main/resources/templates/error.html new file mode 100644 index 0000000000..774b080d6c --- /dev/null +++ b/jhipster/src/main/resources/templates/error.html @@ -0,0 +1,162 @@ + + + + + Your request cannot be processed + + + +
+

Your request cannot be processed :(

+ +

Sorry, an error has occurred.

+ + Status:  ()
+ + Message: 
+
+ + + +
+ + diff --git a/jhipster/src/main/webapp/404.html b/jhipster/src/main/webapp/404.html new file mode 100644 index 0000000000..8d7925a892 --- /dev/null +++ b/jhipster/src/main/webapp/404.html @@ -0,0 +1,60 @@ + + + + + Page Not Found + + + + +

Page Not Found

+

Sorry, but the page you were trying to view does not exist.

+ + + diff --git a/jhipster/src/main/webapp/app/account/account.module.ts b/jhipster/src/main/webapp/app/account/account.module.ts new file mode 100644 index 0000000000..09b19a7555 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/account.module.ts @@ -0,0 +1,45 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { BaeldungSharedModule } from '../shared'; + +import { + Register, + Activate, + Password, + PasswordResetInit, + PasswordResetFinish, + PasswordStrengthBarComponent, + RegisterComponent, + ActivateComponent, + PasswordComponent, + PasswordResetInitComponent, + PasswordResetFinishComponent, + SettingsComponent, + accountState +} from './'; + +@NgModule({ + imports: [ + BaeldungSharedModule, + RouterModule.forRoot(accountState, { useHash: true }) + ], + declarations: [ + ActivateComponent, + RegisterComponent, + PasswordComponent, + PasswordStrengthBarComponent, + PasswordResetInitComponent, + PasswordResetFinishComponent, + SettingsComponent + ], + providers: [ + Register, + Activate, + Password, + PasswordResetInit, + PasswordResetFinish + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BaeldungAccountModule {} diff --git a/jhipster/src/main/webapp/app/account/account.route.ts b/jhipster/src/main/webapp/app/account/account.route.ts new file mode 100644 index 0000000000..4715216cf3 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/account.route.ts @@ -0,0 +1,26 @@ +import { Routes, CanActivate } from '@angular/router'; + +import { UserRouteAccessService } from '../shared'; + +import { + activateRoute, + passwordRoute, + passwordResetFinishRoute, + passwordResetInitRoute, + registerRoute, + settingsRoute +} from './'; + +let ACCOUNT_ROUTES = [ + activateRoute, + passwordRoute, + passwordResetFinishRoute, + passwordResetInitRoute, + registerRoute, + settingsRoute +]; + +export const accountState: Routes = [{ + path: '', + children: ACCOUNT_ROUTES +}]; diff --git a/jhipster/src/main/webapp/app/account/activate/activate.component.html b/jhipster/src/main/webapp/app/account/activate/activate.component.html new file mode 100644 index 0000000000..25e3f23417 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/activate/activate.component.html @@ -0,0 +1,19 @@ +
+
+
+

Activation

+ +
+ + Your user has been activated. Please + sign in. + +
+ +
+ Your user could not be activated. Please use the registration form to sign up. +
+ +
+
+
diff --git a/jhipster/src/main/webapp/app/account/activate/activate.component.ts b/jhipster/src/main/webapp/app/account/activate/activate.component.ts new file mode 100644 index 0000000000..dbaaa7d676 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/activate/activate.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute } from '@angular/router'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { Activate } from './activate.service'; +import { LoginModalService } from '../../shared'; + +@Component({ + selector: 'jhi-activate', + templateUrl: './activate.component.html' +}) +export class ActivateComponent implements OnInit { + error: string; + success: string; + modalRef: NgbModalRef; + + constructor( + private jhiLanguageService: JhiLanguageService, + private activate: Activate, + private loginModalService: LoginModalService, + private route: ActivatedRoute + ) { + this.jhiLanguageService.setLocations(['activate']); + } + + ngOnInit () { + this.route.queryParams.subscribe(params => { + this.activate.get(params['key']).subscribe(() => { + this.error = null; + this.success = 'OK'; + }, () => { + this.success = null; + this.error = 'ERROR'; + }); + }); + } + + login() { + this.modalRef = this.loginModalService.open(); + } +} diff --git a/jhipster/src/main/webapp/app/account/activate/activate.route.ts b/jhipster/src/main/webapp/app/account/activate/activate.route.ts new file mode 100644 index 0000000000..8f1bb32b58 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/activate/activate.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { ActivateComponent } from './activate.component'; + +export const activateRoute: Route = { + path: 'activate', + component: ActivateComponent, + data: { + authorities: [], + pageTitle: 'activate.title' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/account/activate/activate.service.ts b/jhipster/src/main/webapp/app/account/activate/activate.service.ts new file mode 100644 index 0000000000..f877a4d50e --- /dev/null +++ b/jhipster/src/main/webapp/app/account/activate/activate.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, URLSearchParams } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class Activate { + + constructor (private http: Http) {} + + get(key: string): Observable { + let params: URLSearchParams = new URLSearchParams(); + params.set('key', key); + + return this.http.get('api/activate', { + search: params + }).map((res: Response) => res); + } +} diff --git a/jhipster/src/main/webapp/app/account/index.ts b/jhipster/src/main/webapp/app/account/index.ts new file mode 100644 index 0000000000..aeada0551c --- /dev/null +++ b/jhipster/src/main/webapp/app/account/index.ts @@ -0,0 +1,19 @@ +export * from './activate/activate.component'; +export * from './activate/activate.service'; +export * from './activate/activate.route'; +export * from './password/password.component'; +export * from './password/password-strength-bar.component'; +export * from './password/password.service'; +export * from './password/password.route'; +export * from './password-reset/finish/password-reset-finish.component'; +export * from './password-reset/finish/password-reset-finish.service'; +export * from './password-reset/finish/password-reset-finish.route'; +export * from './password-reset/init/password-reset-init.component'; +export * from './password-reset/init/password-reset-init.service'; +export * from './password-reset/init/password-reset-init.route'; +export * from './register/register.component'; +export * from './register/register.service'; +export * from './register/register.route'; +export * from './settings/settings.component'; +export * from './settings/settings.route'; +export * from './account.route'; diff --git a/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html new file mode 100644 index 0000000000..a1dfde055c --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html @@ -0,0 +1,77 @@ +
+
+
+

Reset password

+ +
+ The password reset key is missing. +
+ +
+

Choose a new password

+
+ +
+

Your password couldn't be reset. Remember a password request is only valid for 24 hours.

+
+ +

+ Your password has been reset. Please + sign in. +

+ +
+ The password and its confirmation do not match! +
+ +
+
+
+ + +
+ + Your password is required. + + + Your password is required to be at least 4 characters. + + + Your password cannot be longer than 50 characters. + +
+ +
+ +
+ + +
+ + Your password confirmation is required. + + + Your password confirmation is required to be at least 4 characters. + + + Your password confirmation cannot be longer than 50 characters. + +
+
+ +
+
+ +
+
+
diff --git a/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts new file mode 100644 index 0000000000..f1889920bd --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit, AfterViewInit, Renderer, ElementRef } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute } from '@angular/router'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { PasswordResetFinish } from './password-reset-finish.service'; +import { LoginModalService } from '../../../shared'; + +@Component({ + selector: 'jhi-password-reset-finish', + templateUrl: './password-reset-finish.component.html' +}) +export class PasswordResetFinishComponent implements OnInit, AfterViewInit { + confirmPassword: string; + doNotMatch: string; + error: string; + keyMissing: boolean; + resetAccount: any; + success: string; + modalRef: NgbModalRef; + key: string; + + constructor( + private jhiLanguageService: JhiLanguageService, + private passwordResetFinish: PasswordResetFinish, + private loginModalService: LoginModalService, + private route: ActivatedRoute, + private elementRef: ElementRef, private renderer: Renderer + ) { + this.jhiLanguageService.setLocations(['reset']); + } + + ngOnInit() { + this.route.queryParams.subscribe(params => { + this.key = params['key']; + }); + this.resetAccount = {}; + this.keyMissing = !this.key; + } + + ngAfterViewInit() { + if (this.elementRef.nativeElement.querySelector('#password') != null) { + this.renderer.invokeElementMethod(this.elementRef.nativeElement.querySelector('#password'), 'focus', []); + } + } + + finishReset() { + this.doNotMatch = null; + this.error = null; + if (this.resetAccount.password !== this.confirmPassword) { + this.doNotMatch = 'ERROR'; + } else { + this.passwordResetFinish.save({key: this.key, newPassword: this.resetAccount.password}).subscribe(() => { + this.success = 'OK'; + }, () => { + this.success = null; + this.error = 'ERROR'; + }); + } + } + + login() { + this.modalRef = this.loginModalService.open(); + } +} diff --git a/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts new file mode 100644 index 0000000000..7b02653ed5 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../../shared'; +import { PasswordResetFinishComponent } from './password-reset-finish.component'; + +export const passwordResetFinishRoute: Route = { + path: 'reset/finish', + component: PasswordResetFinishComponent, + data: { + authorities: [], + pageTitle: 'global.menu.account.password' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts new file mode 100644 index 0000000000..abd81374b0 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class PasswordResetFinish { + + constructor (private http: Http) {} + + save(keyAndPassword: any): Observable { + return this.http.post('api/account/reset_password/finish', keyAndPassword); + } +} diff --git a/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html new file mode 100644 index 0000000000..2503433275 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html @@ -0,0 +1,47 @@ +
+
+
+

Reset your password

+ +
+ E-Mail address isn't registered! Please check and try again. +
+ +
+

Enter the e-mail address you used to register.

+
+ +
+

Check your e-mails for details on how to reset your password.

+
+ +
+
+ + +
+ + Your e-mail is required. + + + Your e-mail is invalid. + + + Your e-mail is required to be at least 5 characters. + + + Your e-mail cannot be longer than 100 characters. + +
+
+ +
+ +
+
+
diff --git a/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts new file mode 100644 index 0000000000..eba521dc05 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts @@ -0,0 +1,49 @@ +import { Component, OnInit, AfterViewInit, Renderer, ElementRef } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { PasswordResetInit } from './password-reset-init.service'; + +@Component({ + selector: 'jhi-password-reset-init', + templateUrl: './password-reset-init.component.html' +}) +export class PasswordResetInitComponent implements OnInit, AfterViewInit { + error: string; + errorEmailNotExists: string; + resetAccount: any; + success: string; + + constructor( + private jhiLanguageService: JhiLanguageService, + private passwordResetInit: PasswordResetInit, + private elementRef: ElementRef, + private renderer: Renderer + ) { + this.jhiLanguageService.setLocations(['reset']); + } + + ngOnInit() { + this.resetAccount = {}; + } + + ngAfterViewInit() { + this.renderer.invokeElementMethod(this.elementRef.nativeElement.querySelector('#email'), 'focus', []); + } + + requestReset () { + + this.error = null; + this.errorEmailNotExists = null; + + this.passwordResetInit.save(this.resetAccount.email).subscribe(() => { + this.success = 'OK'; + }, (response) => { + this.success = null; + if (response.status === 400 && response.data === 'e-mail address not registered') { + this.errorEmailNotExists = 'ERROR'; + } else { + this.error = 'ERROR'; + } + }); + } +} diff --git a/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts new file mode 100644 index 0000000000..37f7e8102d --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../../shared'; +import { PasswordResetInitComponent } from './password-reset-init.component'; + +export const passwordResetInitRoute: Route = { + path: 'reset/request', + component: PasswordResetInitComponent, + data: { + authorities: [], + pageTitle: 'global.menu.account.password' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts new file mode 100644 index 0000000000..fa466cd6cc --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class PasswordResetInit { + + constructor (private http: Http) {} + + save(mail: string): Observable { + return this.http.post('api/account/reset_password/init', mail); + } +} diff --git a/jhipster/src/main/webapp/app/account/password/password-strength-bar.component.ts b/jhipster/src/main/webapp/app/account/password/password-strength-bar.component.ts new file mode 100644 index 0000000000..2a61d133d6 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password/password-strength-bar.component.ts @@ -0,0 +1,89 @@ +import { Component, ElementRef, Input, Renderer } from '@angular/core'; + +@Component({ + selector: 'jhi-password-strength-bar', + template: ` +
+ Password strength: +
    +
  • +
  • +
  • +
  • +
  • +
+
`, + styleUrls: [ + 'password-strength-bar.scss' + ] +}) +export class PasswordStrengthBarComponent { + + colors = ['#F00', '#F90', '#FF0', '#9F0', '#0F0']; + + constructor(private renderer: Renderer, private elementRef: ElementRef) { } + + measureStrength(p: string): number { + + let force = 0; + let regex = /[$-/:-?{-~!"^_`\[\]]/g; // " + + let lowerLetters = /[a-z]+/.test(p); + let upperLetters = /[A-Z]+/.test(p); + let numbers = /[0-9]+/.test(p); + let symbols = regex.test(p); + + let flags = [lowerLetters, upperLetters, numbers, symbols]; + let passedMatches = flags.filter( (isMatchedFlag: boolean) => { + return isMatchedFlag === true; + }).length; + + force += 2 * p.length + ((p.length >= 10) ? 1 : 0); + force += passedMatches * 10; + + // penality (short password) + force = (p.length <= 6) ? Math.min(force, 10) : force; + + // penality (poor variety of characters) + force = (passedMatches === 1) ? Math.min(force, 10) : force; + force = (passedMatches === 2) ? Math.min(force, 20) : force; + force = (passedMatches === 3) ? Math.min(force, 40) : force; + + return force; + }; + + getColor(s: number): any { + let idx = 0; + if (s <= 10) { + idx = 0; + } else if (s <= 20) { + idx = 1; + } else if (s <= 30) { + idx = 2; + } else if (s <= 40) { + idx = 3; + } else { + idx = 4; + } + return {idx: idx + 1, col: this.colors[idx]}; + }; + + @Input() + set passwordToCheck(password: string) { + if (password) { + let c = this.getColor(this.measureStrength(password)); + let element = this.elementRef.nativeElement; + if ( element.className ) { + this.renderer.setElementClass(element, element.className , false); + } + let lis = element.getElementsByTagName('li'); + for (let i = 0; i < lis.length; i++) { + if (i < c.idx) { + this.renderer.setElementStyle(lis[i], 'backgroundColor', c.col); + } else { + this.renderer.setElementStyle(lis[i], 'backgroundColor', '#DDD'); + } + } + } + } +} diff --git a/jhipster/src/main/webapp/app/account/password/password-strength-bar.scss b/jhipster/src/main/webapp/app/account/password/password-strength-bar.scss new file mode 100644 index 0000000000..8f8effcb60 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password/password-strength-bar.scss @@ -0,0 +1,23 @@ +/* ========================================================================== +start Password strength bar style +========================================================================== */ +ul#strength { + display:inline; + list-style:none; + margin:0; + margin-left:15px; + padding:0; + vertical-align:2px; +} + +.point { + background:#DDD; + border-radius:2px; + display:inline-block; + height:5px; + margin-right:1px; + width:20px; + &:last { + margin:0 !important; + } +} diff --git a/jhipster/src/main/webapp/app/account/password/password.component.html b/jhipster/src/main/webapp/app/account/password/password.component.html new file mode 100644 index 0000000000..2534bccdfc --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password/password.component.html @@ -0,0 +1,65 @@ +
+
+
+

Password for [{{account.login}}]

+ +
+ Password changed! +
+
+ An error has occurred! The password could not be changed. +
+ +
+ The password and its confirmation do not match! +
+ +
+ +
+ + +
+ + Your password is required. + + + Your password is required to be at least 4 characters. + + + Your password cannot be longer than 50 characters. + +
+ +
+
+ + +
+ + Your confirmation password is required. + + + Your confirmation password is required to be at least 4 characters. + + + Your confirmation password cannot be longer than 50 characters. + +
+
+ + +
+
+
+
diff --git a/jhipster/src/main/webapp/app/account/password/password.component.ts b/jhipster/src/main/webapp/app/account/password/password.component.ts new file mode 100644 index 0000000000..f34eae423c --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password/password.component.ts @@ -0,0 +1,49 @@ +import { Component, OnInit } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { Principal } from '../../shared'; +import { Password } from './password.service'; + +@Component({ + selector: 'jhi-password', + templateUrl: './password.component.html' +}) +export class PasswordComponent implements OnInit { + doNotMatch: string; + error: string; + success: string; + account: any; + password: string; + confirmPassword: string; + + constructor( + private jhiLanguageService: JhiLanguageService, + private passwordService: Password, + private principal: Principal + ) { + this.jhiLanguageService.setLocations(['password']); + } + + ngOnInit () { + this.principal.identity().then((account) => { + this.account = account; + }); + } + + changePassword () { + if (this.password !== this.confirmPassword) { + this.error = null; + this.success = null; + this.doNotMatch = 'ERROR'; + } else { + this.doNotMatch = null; + this.passwordService.save(this.password).subscribe(() => { + this.error = null; + this.success = 'OK'; + }, () => { + this.success = null; + this.error = 'ERROR'; + }); + } + } +} diff --git a/jhipster/src/main/webapp/app/account/password/password.route.ts b/jhipster/src/main/webapp/app/account/password/password.route.ts new file mode 100644 index 0000000000..d5a7118458 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password/password.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { PasswordComponent } from './password.component'; + +export const passwordRoute: Route = { + path: 'password', + component: PasswordComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'global.menu.account.password' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/account/password/password.service.ts b/jhipster/src/main/webapp/app/account/password/password.service.ts new file mode 100644 index 0000000000..0c220d8816 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/password/password.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class Password { + + constructor (private http: Http) {} + + save(newPassword: string): Observable { + return this.http.post('api/account/change_password', newPassword); + } +} diff --git a/jhipster/src/main/webapp/app/account/register/register.component.html b/jhipster/src/main/webapp/app/account/register/register.component.html new file mode 100644 index 0000000000..14a0c851e2 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/register/register.component.html @@ -0,0 +1,122 @@ +
+
+
+

Registration

+ +
+ Registration saved! Please check your email for confirmation. +
+ +
+ Registration failed! Please try again later. +
+ +
+ Login name already registered! Please choose another one. +
+ +
+ E-mail is already in use! Please choose another one. +
+ +
+ The password and its confirmation do not match! +
+
+
+
+
+ + +
+ + Your username is required. + + + Your username is required to be at least 1 character. + + + Your username cannot be longer than 50 characters. + + + Your username can only contain lower-case letters and digits. + +
+
+
+ + +
+ + Your e-mail is required. + + + Your e-mail is invalid. + + + Your e-mail is required to be at least 5 characters. + + + Your e-mail cannot be longer than 100 characters. + +
+
+
+ + +
+ + Your password is required. + + + Your password is required to be at least 4 characters. + + + Your password cannot be longer than 50 characters. + +
+ +
+
+ + +
+ + Your confirmation password is required. + + + Your confirmation password is required to be at least 4 characters. + + + Your confirmation password cannot be longer than 50 characters. + +
+
+ + +
+

+
+ If you want to + sign in, you can try the default accounts:
- Administrator (login="admin" and password="admin")
- User (login="user" and password="user").
+
+
+
+
diff --git a/jhipster/src/main/webapp/app/account/register/register.component.ts b/jhipster/src/main/webapp/app/account/register/register.component.ts new file mode 100644 index 0000000000..dc5c4ef2f4 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/register/register.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, AfterViewInit, Renderer, ElementRef } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { Register } from './register.service'; +import { LoginModalService } from '../../shared'; + +@Component({ + selector: 'jhi-register', + templateUrl: './register.component.html' +}) +export class RegisterComponent implements OnInit, AfterViewInit { + + confirmPassword: string; + doNotMatch: string; + error: string; + errorEmailExists: string; + errorUserExists: string; + registerAccount: any; + success: boolean; + modalRef: NgbModalRef; + + constructor( + private languageService: JhiLanguageService, + private loginModalService: LoginModalService, + private registerService: Register, + private elementRef: ElementRef, + private renderer: Renderer + ) { + this.languageService.setLocations(['register']); + } + + ngOnInit() { + this.success = false; + this.registerAccount = {}; + } + + ngAfterViewInit() { + this.renderer.invokeElementMethod(this.elementRef.nativeElement.querySelector('#login'), 'focus', []); + } + + register() { + if (this.registerAccount.password !== this.confirmPassword) { + this.doNotMatch = 'ERROR'; + } else { + this.doNotMatch = null; + this.error = null; + this.errorUserExists = null; + this.errorEmailExists = null; + this.languageService.getCurrent().then(key => { + this.registerAccount.langKey = key; + this.registerService.save(this.registerAccount).subscribe(() => { + this.success = true; + }, (response) => this.processError(response)); + }); + } + } + + openLogin() { + this.modalRef = this.loginModalService.open(); + } + + private processError(response) { + this.success = null; + if (response.status === 400 && response._body === 'login already in use') { + this.errorUserExists = 'ERROR'; + } else if (response.status === 400 && response._body === 'e-mail address already in use') { + this.errorEmailExists = 'ERROR'; + } else { + this.error = 'ERROR'; + } + } +} diff --git a/jhipster/src/main/webapp/app/account/register/register.route.ts b/jhipster/src/main/webapp/app/account/register/register.route.ts new file mode 100644 index 0000000000..3dc47f1e58 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/register/register.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { RegisterComponent } from './register.component'; + +export const registerRoute: Route = { + path: 'register', + component: RegisterComponent, + data: { + authorities: [], + pageTitle: 'register.title' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/account/register/register.service.ts b/jhipster/src/main/webapp/app/account/register/register.service.ts new file mode 100644 index 0000000000..87bbe9d37b --- /dev/null +++ b/jhipster/src/main/webapp/app/account/register/register.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class Register { + + constructor (private http: Http) {} + + save(account: any): Observable { + return this.http.post('api/register', account); + } +} diff --git a/jhipster/src/main/webapp/app/account/settings/settings.component.html b/jhipster/src/main/webapp/app/account/settings/settings.component.html new file mode 100644 index 0000000000..82fca9e804 --- /dev/null +++ b/jhipster/src/main/webapp/app/account/settings/settings.component.html @@ -0,0 +1,86 @@ +
+
+
+

User settings for [{{settingsAccount.login}}]

+ +
+ Settings saved! +
+ + + +
+ +
+ + +
+ + Your first name is required. + + + Your first name is required to be at least 1 character. + + + Your first name cannot be longer than 50 characters. + +
+
+
+ + +
+ + Your last name is required. + + + Your last name is required to be at least 1 character. + + + Your last name cannot be longer than 50 characters. + +
+
+
+ + +
+ + Your e-mail is required. + + + Your e-mail is invalid. + + + Your e-mail is required to be at least 5 characters. + + + Your e-mail cannot be longer than 100 characters. + +
+
+
+ + +
+ +
+
+
+ +
diff --git a/jhipster/src/main/webapp/app/account/settings/settings.component.ts b/jhipster/src/main/webapp/app/account/settings/settings.component.ts new file mode 100644 index 0000000000..37f4c70e3b --- /dev/null +++ b/jhipster/src/main/webapp/app/account/settings/settings.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { Principal, AccountService, JhiLanguageHelper } from '../../shared'; + +@Component({ + selector: 'jhi-settings', + templateUrl: './settings.component.html' +}) +export class SettingsComponent implements OnInit { + error: string; + success: string; + settingsAccount: any; + languages: any[]; + + constructor( + private account: AccountService, + private principal: Principal, + private languageService: JhiLanguageService, + private languageHelper: JhiLanguageHelper + ) { + this.languageService.setLocations(['settings']); + } + + ngOnInit () { + this.principal.identity().then((account) => { + this.settingsAccount = this.copyAccount(account); + }); + this.languageHelper.getAll().then((languages) => { + this.languages = languages; + }); + } + + save () { + this.account.save(this.settingsAccount).subscribe(() => { + this.error = null; + this.success = 'OK'; + this.principal.identity(true).then((account) => { + this.settingsAccount = this.copyAccount(account); + }); + this.languageService.getCurrent().then((current) => { + if (this.settingsAccount.langKey !== current) { + this.languageService.changeLanguage(this.settingsAccount.langKey); + } + }); + }, () => { + this.success = null; + this.error = 'ERROR'; + }); + } + + copyAccount (account) { + return { + activated: account.activated, + email: account.email, + firstName: account.firstName, + langKey: account.langKey, + lastName: account.lastName, + login: account.login, + imageUrl: account.imageUrl + }; + } +} diff --git a/jhipster/src/main/webapp/app/account/settings/settings.route.ts b/jhipster/src/main/webapp/app/account/settings/settings.route.ts new file mode 100644 index 0000000000..9c9a852c7b --- /dev/null +++ b/jhipster/src/main/webapp/app/account/settings/settings.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { SettingsComponent } from './settings.component'; + +export const settingsRoute: Route = { + path: 'settings', + component: SettingsComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'global.menu.account.settings' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/admin/admin.module.ts b/jhipster/src/main/webapp/app/admin/admin.module.ts new file mode 100644 index 0000000000..4ac749bc78 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/admin.module.ts @@ -0,0 +1,73 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ParseLinks } from 'ng-jhipster'; + +import { BaeldungSharedModule } from '../shared'; + +import { + adminState, + AuditsComponent, + UserMgmtComponent, + UserDialogComponent, + UserDeleteDialogComponent, + UserMgmtDetailComponent, + UserMgmtDialogComponent, + UserMgmtDeleteDialogComponent, + LogsComponent, + JhiMetricsMonitoringModalComponent, + JhiMetricsMonitoringComponent, + JhiHealthModalComponent, + JhiHealthCheckComponent, + JhiConfigurationComponent, + JhiDocsComponent, + AuditsService, + JhiConfigurationService, + JhiHealthService, + JhiMetricsService, + LogsService, + UserResolvePagingParams, + UserResolve, + UserModalService +} from './'; + + +@NgModule({ + imports: [ + BaeldungSharedModule, + RouterModule.forRoot(adminState, { useHash: true }) + ], + declarations: [ + AuditsComponent, + UserMgmtComponent, + UserDialogComponent, + UserDeleteDialogComponent, + UserMgmtDetailComponent, + UserMgmtDialogComponent, + UserMgmtDeleteDialogComponent, + LogsComponent, + JhiConfigurationComponent, + JhiHealthCheckComponent, + JhiHealthModalComponent, + JhiDocsComponent, + JhiMetricsMonitoringComponent, + JhiMetricsMonitoringModalComponent + ], + entryComponents: [ + UserMgmtDialogComponent, + UserMgmtDeleteDialogComponent, + JhiHealthModalComponent, + JhiMetricsMonitoringModalComponent, + ], + providers: [ + AuditsService, + JhiConfigurationService, + JhiHealthService, + JhiMetricsService, + LogsService, + UserResolvePagingParams, + UserResolve, + UserModalService + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BaeldungAdminModule {} diff --git a/jhipster/src/main/webapp/app/admin/admin.route.ts b/jhipster/src/main/webapp/app/admin/admin.route.ts new file mode 100644 index 0000000000..8e64d2d5c0 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/admin.route.ts @@ -0,0 +1,36 @@ +import { Routes, CanActivate } from '@angular/router'; + +import { + auditsRoute, + configurationRoute, + docsRoute, + healthRoute, + logsRoute, + metricsRoute, + userMgmtRoute, + userDialogRoute +} from './'; + +import { UserRouteAccessService } from '../shared'; + +let ADMIN_ROUTES = [ + auditsRoute, + configurationRoute, + docsRoute, + healthRoute, + logsRoute, + ...userMgmtRoute, + metricsRoute +]; + + +export const adminState: Routes = [{ + path: '', + data: { + authorities: ['ROLE_ADMIN'] + }, + canActivate: [UserRouteAccessService], + children: ADMIN_ROUTES +}, + ...userDialogRoute +]; diff --git a/jhipster/src/main/webapp/app/admin/audits/audit-data.model.ts b/jhipster/src/main/webapp/app/admin/audits/audit-data.model.ts new file mode 100644 index 0000000000..55a5e9c819 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/audits/audit-data.model.ts @@ -0,0 +1,6 @@ +export class AuditData { + constructor( + public remoteAddress: string, + public sessionId: string + ) { } +} diff --git a/jhipster/src/main/webapp/app/admin/audits/audit.model.ts b/jhipster/src/main/webapp/app/admin/audits/audit.model.ts new file mode 100644 index 0000000000..9ca753795c --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/audits/audit.model.ts @@ -0,0 +1,10 @@ +import { AuditData } from './audit-data.model'; + +export class Audit { + constructor( + public data: AuditData, + public principal: string, + public timestamp: string, + public type: string + ) { } +} diff --git a/jhipster/src/main/webapp/app/admin/audits/audits.component.html b/jhipster/src/main/webapp/app/admin/audits/audits.component.html new file mode 100644 index 0000000000..9fcbaf7ecd --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/audits/audits.component.html @@ -0,0 +1,45 @@ +
+

Audits

+ +
+
+

Filter by date

+

+ from + + to + +

+
+
+ +
+ + + + + + + + + + + + + + + +
DateUserStateExtra data
{{audit.timestamp| date:'medium'}}{{audit.principal}}{{audit.type}} + {{audit.data.message}} + Remote Address {{audit.data.remoteAddress}} +
+
+
+
+ +
+
+ +
+
+
diff --git a/jhipster/src/main/webapp/app/admin/audits/audits.component.ts b/jhipster/src/main/webapp/app/admin/audits/audits.component.ts new file mode 100644 index 0000000000..84e6b40fe8 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/audits/audits.component.ts @@ -0,0 +1,99 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { ParseLinks, JhiLanguageService} from 'ng-jhipster'; + +import { Audit } from './audit.model'; +import { AuditsService } from './audits.service'; +import { ITEMS_PER_PAGE } from '../../shared'; +import { PaginationConfig } from '../../blocks/config/uib-pagination.config'; + +@Component({ + selector: 'jhi-audit', + templateUrl: './audits.component.html' +}) +export class AuditsComponent implements OnInit { + audits: Audit[]; + fromDate: string; + itemsPerPage: any; + links: any; + page: number; + orderProp: string; + reverse: boolean; + toDate: string; + totalItems: number; + + constructor( + private jhiLanguageService: JhiLanguageService, + private auditsService: AuditsService, + private parseLinks: ParseLinks, + private paginationConfig: PaginationConfig, + private datePipe: DatePipe + ) { + this.jhiLanguageService.setLocations(['audits']); + this.itemsPerPage = ITEMS_PER_PAGE; + this.page = 1; + this.reverse = false; + this.orderProp = 'timestamp'; + } + + getAudits() { + return this.sortAudits(this.audits); + } + + loadPage(page: number) { + this.page = page; + this.onChangeDate(); + } + + ngOnInit() { + this.today(); + this.previousMonth(); + this.onChangeDate(); + } + + onChangeDate() { + this.auditsService.query({page: this.page - 1, size: this.itemsPerPage, + fromDate: this.fromDate, toDate: this.toDate}).subscribe(res => { + + this.audits = res.json(); + this.links = this.parseLinks.parse(res.headers.get('link')); + this.totalItems = + res.headers.get('X-Total-Count'); + }); + } + + previousMonth() { + let dateFormat = 'yyyy-MM-dd'; + let fromDate: Date = new Date(); + + if (fromDate.getMonth() === 0) { + fromDate = new Date(fromDate.getFullYear() - 1, 11, fromDate.getDate()); + } else { + fromDate = new Date(fromDate.getFullYear(), fromDate.getMonth() - 1, fromDate.getDate()); + } + + this.fromDate = this.datePipe.transform(fromDate, dateFormat); + } + + today() { + let dateFormat = 'yyyy-MM-dd'; + // Today + 1 day - needed if the current day must be included + let today: Date = new Date(); + + let date = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1); + this.toDate = this.datePipe.transform(date, dateFormat); + } + + private sortAudits(audits: Audit[]) { + audits = audits.slice(0).sort((a, b) => { + if (a[this.orderProp] < b[this.orderProp]) { + return -1; + } else if ([b[this.orderProp] < a[this.orderProp]]) { + return 1; + } else { + return 0; + } + }); + + return this.reverse ? audits.reverse() : audits; + } +} diff --git a/jhipster/src/main/webapp/app/admin/audits/audits.route.ts b/jhipster/src/main/webapp/app/admin/audits/audits.route.ts new file mode 100644 index 0000000000..f0e0fa7be9 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/audits/audits.route.ts @@ -0,0 +1,12 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { AuditsComponent } from './audits.component'; + +export const auditsRoute: Route = { + path: 'audits', + component: AuditsComponent, + data: { + pageTitle: 'audits.title' + } +}; diff --git a/jhipster/src/main/webapp/app/admin/audits/audits.service.ts b/jhipster/src/main/webapp/app/admin/audits/audits.service.ts new file mode 100644 index 0000000000..b373b8915c --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/audits/audits.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, URLSearchParams } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class AuditsService { + constructor(private http: Http) { } + + query(req: any): Observable { + let params: URLSearchParams = new URLSearchParams(); + params.set('fromDate', req.fromDate); + params.set('toDate', req.toDate); + params.set('page', req.page); + params.set('size', req.size); + params.set('sort', req.sort); + + let options = { + search: params + }; + + return this.http.get('management/audits', options); + } +} diff --git a/jhipster/src/main/webapp/app/admin/configuration/configuration.component.html b/jhipster/src/main/webapp/app/admin/configuration/configuration.component.html new file mode 100644 index 0000000000..1d64b41c29 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/configuration/configuration.component.html @@ -0,0 +1,46 @@ +
+

Configuration

+ + Filter (by prefix) + + + + + + + + + + + + + + +
PrefixProperties
{{entry.prefix}} +
+
{{key}}
+
+ {{entry.properties[key]}} +
+
+
+
+ + + + + + + + + + + + + + +
PropertyValue
{{item.key}} + {{item.val}} +
+
+
diff --git a/jhipster/src/main/webapp/app/admin/configuration/configuration.component.ts b/jhipster/src/main/webapp/app/admin/configuration/configuration.component.ts new file mode 100644 index 0000000000..88fbc32250 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/configuration/configuration.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { JhiConfigurationService } from './configuration.service'; + +@Component({ + selector: 'jhi-configuration', + templateUrl: './configuration.component.html' +}) +export class JhiConfigurationComponent implements OnInit { + allConfiguration: any = null; + configuration: any = null; + configKeys: any[]; + filter: string; + orderProp: string; + reverse: boolean; + + constructor( + private jhiLanguageService: JhiLanguageService, + private configurationService: JhiConfigurationService + ) { + this.jhiLanguageService.setLocations(['configuration']); + this.configKeys = []; + this.filter = ''; + this.orderProp = 'prefix'; + this.reverse = false; + } + + keys(dict): Array { + return (dict === undefined) ? [] : Object.keys(dict); + } + + ngOnInit() { + this.configurationService.get().subscribe((configuration) => { + this.configuration = configuration; + + for (let config of configuration) { + if (config.properties !== undefined) { + this.configKeys.push(Object.keys(config.properties)); + } + } + }); + + this.configurationService.getEnv().subscribe((configuration) => { + this.allConfiguration = configuration; + }); + } +} diff --git a/jhipster/src/main/webapp/app/admin/configuration/configuration.route.ts b/jhipster/src/main/webapp/app/admin/configuration/configuration.route.ts new file mode 100644 index 0000000000..06866237b0 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/configuration/configuration.route.ts @@ -0,0 +1,12 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { JhiConfigurationComponent } from './configuration.component'; + +export const configurationRoute: Route = { + path: 'jhi-configuration', + component: JhiConfigurationComponent, + data: { + pageTitle: 'configuration.title' + } +}; diff --git a/jhipster/src/main/webapp/app/admin/configuration/configuration.service.ts b/jhipster/src/main/webapp/app/admin/configuration/configuration.service.ts new file mode 100644 index 0000000000..e239ed3097 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/configuration/configuration.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class JhiConfigurationService { + + constructor(private http: Http) { + } + + get(): Observable { + return this.http.get('management/configprops').map((res: Response) => { + let properties: any[] = []; + + const propertiesObject = res.json(); + + for (let key in propertiesObject) { + if (propertiesObject.hasOwnProperty(key)) { + properties.push(propertiesObject[key]); + } + } + + return properties.sort((propertyA, propertyB) => { + return (propertyA.prefix === propertyB.prefix) ? 0 : + (propertyA.prefix < propertyB.prefix) ? -1 : 1; + }); + }); + } + + getEnv(): Observable { + return this.http.get('management/env').map((res: Response) => { + let properties: any = {}; + + const propertiesObject = res.json(); + + for (let key in propertiesObject) { + if (propertiesObject.hasOwnProperty(key)) { + let valsObject = propertiesObject[key]; + let vals: any[] = []; + + for (let valKey in valsObject) { + if (valsObject.hasOwnProperty(valKey)) { + vals.push({key: valKey, val: valsObject[valKey]}); + } + } + properties[key] = vals; + } + } + + return properties; + }); + } +} diff --git a/jhipster/src/main/webapp/app/admin/docs/docs.component.html b/jhipster/src/main/webapp/app/admin/docs/docs.component.html new file mode 100644 index 0000000000..30efbbb93e --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/docs/docs.component.html @@ -0,0 +1,2 @@ + diff --git a/jhipster/src/main/webapp/app/admin/docs/docs.component.ts b/jhipster/src/main/webapp/app/admin/docs/docs.component.ts new file mode 100644 index 0000000000..4bd5bf2329 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/docs/docs.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +@Component({ + selector: 'jhi-docs', + templateUrl: './docs.component.html' +}) +export class JhiDocsComponent { + constructor ( + private jhiLanguageService: JhiLanguageService + ) { + this.jhiLanguageService.setLocations(['global']); + } +} diff --git a/jhipster/src/main/webapp/app/admin/docs/docs.route.ts b/jhipster/src/main/webapp/app/admin/docs/docs.route.ts new file mode 100644 index 0000000000..6e99618f1d --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/docs/docs.route.ts @@ -0,0 +1,12 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { JhiDocsComponent } from './docs.component'; + +export const docsRoute: Route = { + path: 'docs', + component: JhiDocsComponent, + data: { + pageTitle: 'global.menu.admin.apidocs' + } +}; diff --git a/jhipster/src/main/webapp/app/admin/health/health-modal.component.html b/jhipster/src/main/webapp/app/admin/health/health-modal.component.html new file mode 100644 index 0000000000..1be6271f3b --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/health/health-modal.component.html @@ -0,0 +1,36 @@ + + + diff --git a/jhipster/src/main/webapp/app/admin/health/health-modal.component.ts b/jhipster/src/main/webapp/app/admin/health/health-modal.component.ts new file mode 100644 index 0000000000..1486f20dbf --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/health/health-modal.component.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { JhiHealthService } from './health.service'; + +@Component({ + selector: 'jhi-health-modal', + templateUrl: './health-modal.component.html' +}) +export class JhiHealthModalComponent { + + currentHealth: any; + + constructor(private healthService: JhiHealthService, public activeModal: NgbActiveModal) {} + + baseName(name) { + return this.healthService.getBaseName(name); + } + + subSystemName(name) { + return this.healthService.getSubSystemName(name); + } + + readableValue(value: number) { + if (this.currentHealth.name !== 'diskSpace') { + return value.toString(); + } + + // Should display storage space in an human readable unit + let val = value / 1073741824; + if (val > 1) { // Value + return val.toFixed(2) + ' GB'; + } else { + return (value / 1048576).toFixed(2) + ' MB'; + } + } +} diff --git a/jhipster/src/main/webapp/app/admin/health/health.component.html b/jhipster/src/main/webapp/app/admin/health/health.component.html new file mode 100644 index 0000000000..6fea72af96 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/health/health.component.html @@ -0,0 +1,34 @@ +
+

+ Health Checks + +

+
+ + + + + + + + + + + + + + + +
Service NameStatusDetails
{{'health.indicator.' + baseName(health.name) | translate}} {{subSystemName(health.name)}} + + {{health.status}} + + + + + +
+
+
diff --git a/jhipster/src/main/webapp/app/admin/health/health.component.ts b/jhipster/src/main/webapp/app/admin/health/health.component.ts new file mode 100644 index 0000000000..ab1be4271f --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/health/health.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { JhiHealthService } from './health.service'; +import { JhiHealthModalComponent } from './health-modal.component'; + +@Component({ + selector: 'jhi-health', + templateUrl: './health.component.html', +}) +export class JhiHealthCheckComponent implements OnInit { + healthData: any; + updatingHealth: boolean; + + constructor( + private jhiLanguageService: JhiLanguageService, + private modalService: NgbModal, + private healthService: JhiHealthService + ) { + this.jhiLanguageService.setLocations(['health']); + + } + + ngOnInit() { + this.refresh(); + } + + baseName(name: string) { + return this.healthService.getBaseName(name); + } + + getBadgeClass(statusState) { + if (statusState === 'UP') { + return 'badge-success'; + } else { + return 'badge-danger'; + } + } + + refresh() { + this.updatingHealth = true; + + this.healthService.checkHealth().subscribe(health => { + this.healthData = this.healthService.transformHealthData(health); + this.updatingHealth = false; + }, error => { + if (error.status === 503) { + this.healthData = this.healthService.transformHealthData(error.json()); + this.updatingHealth = false; + } + }); + } + + showHealth(health: any) { + const modalRef = this.modalService.open(JhiHealthModalComponent); + modalRef.componentInstance.currentHealth = health; + modalRef.result.then((result) => { + // Left blank intentionally, nothing to do here + }, (reason) => { + // Left blank intentionally, nothing to do here + }); + } + + subSystemName(name: string) { + return this.healthService.getSubSystemName(name); + } + +} diff --git a/jhipster/src/main/webapp/app/admin/health/health.route.ts b/jhipster/src/main/webapp/app/admin/health/health.route.ts new file mode 100644 index 0000000000..b47d07a0c0 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/health/health.route.ts @@ -0,0 +1,12 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { JhiHealthCheckComponent } from './health.component'; + +export const healthRoute: Route = { + path: 'jhi-health', + component: JhiHealthCheckComponent, + data: { + pageTitle: 'health.title' + } +}; diff --git a/jhipster/src/main/webapp/app/admin/health/health.service.ts b/jhipster/src/main/webapp/app/admin/health/health.service.ts new file mode 100644 index 0000000000..1bf00dab08 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/health/health.service.ts @@ -0,0 +1,140 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class JhiHealthService { + + separator: string; + + constructor (private http: Http) { + this.separator = '.'; + } + + checkHealth(): Observable { + return this.http.get('management/health').map((res: Response) => res.json()); + } + + transformHealthData(data): any { + let response = []; + this.flattenHealthData(response, null, data); + return response; + } + + getBaseName(name): string { + if (name) { + let split = name.split('.'); + return split[0]; + } + } + + getSubSystemName(name): string { + if (name) { + let split = name.split('.'); + split.splice(0, 1); + let remainder = split.join('.'); + return remainder ? ' - ' + remainder : ''; + } + } + + /* private methods */ + private addHealthObject(result, isLeaf, healthObject, name): any { + + let status: any; + let error: any; + let healthData: any = { + 'name': name, + 'error': error, + 'status': status + }; + + let details = {}; + let hasDetails = false; + + for (let key in healthObject) { + if (healthObject.hasOwnProperty(key)) { + let value = healthObject[key]; + if (key === 'status' || key === 'error') { + healthData[key] = value; + } else { + if (!this.isHealthObject(value)) { + details[key] = value; + hasDetails = true; + } + } + } + } + + // Add the details + if (hasDetails) { + healthData.details = details; + } + + // Only add nodes if they provide additional information + if (isLeaf || hasDetails || healthData.error) { + result.push(healthData); + } + return healthData; + } + + private flattenHealthData (result, path, data): any { + for (let key in data) { + if (data.hasOwnProperty(key)) { + let value = data[key]; + if (this.isHealthObject(value)) { + if (this.hasSubSystem(value)) { + this.addHealthObject(result, false, value, this.getModuleName(path, key)); + this.flattenHealthData(result, this.getModuleName(path, key), value); + } else { + this.addHealthObject(result, true, value, this.getModuleName(path, key)); + } + } + } + } + + return result; + } + + private getModuleName (path, name): string { + let result; + if (path && name) { + result = path + this.separator + name; + } else if (path) { + result = path; + } else if (name) { + result = name; + } else { + result = ''; + } + return result; + } + + private hasSubSystem (healthObject): boolean { + let result = false; + + for (let key in healthObject) { + if (healthObject.hasOwnProperty(key)) { + let value = healthObject[key]; + if (value && value.status) { + result = true; + } + } + } + + return result; + } + + private isHealthObject (healthObject): boolean { + let result = false; + + for (let key in healthObject) { + if (healthObject.hasOwnProperty(key)) { + if (key === 'status') { + result = true; + } + } + } + + return result; + } +} diff --git a/jhipster/src/main/webapp/app/admin/index.ts b/jhipster/src/main/webapp/app/admin/index.ts new file mode 100644 index 0000000000..2273c8af12 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/index.ts @@ -0,0 +1,29 @@ +export * from './audits/audits.component'; +export * from './audits/audits.service'; +export * from './audits/audits.route'; +export * from './audits/audit.model'; +export * from './audits/audit-data.model'; +export * from './configuration/configuration.component'; +export * from './configuration/configuration.service'; +export * from './configuration/configuration.route'; +export * from './docs/docs.component'; +export * from './docs/docs.route'; +export * from './health/health.component'; +export * from './health/health-modal.component'; +export * from './health/health.service'; +export * from './health/health.route'; +export * from './logs/logs.component'; +export * from './logs/logs.service'; +export * from './logs/logs.route'; +export * from './logs/log.model'; +export * from './metrics/metrics.component'; +export * from './metrics/metrics-modal.component'; +export * from './metrics/metrics.service'; +export * from './metrics/metrics.route'; +export * from './user-management/user-management-dialog.component'; +export * from './user-management/user-management-delete-dialog.component'; +export * from './user-management/user-management-detail.component'; +export * from './user-management/user-management.component'; +export * from './user-management/user-management.route'; +export * from './user-management/user-modal.service'; +export * from './admin.route'; diff --git a/jhipster/src/main/webapp/app/admin/logs/log.model.ts b/jhipster/src/main/webapp/app/admin/logs/log.model.ts new file mode 100644 index 0000000000..79cbd34808 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/logs/log.model.ts @@ -0,0 +1,6 @@ +export class Log { + constructor( + public name: string, + public level: string + ) { } +} diff --git a/jhipster/src/main/webapp/app/admin/logs/logs.component.html b/jhipster/src/main/webapp/app/admin/logs/logs.component.html new file mode 100644 index 0000000000..242ef5563a --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/logs/logs.component.html @@ -0,0 +1,27 @@ +
+

Logs

+ +

There are {{ loggers.length }} loggers.

+ + Filter + + + + + + + + + + + + + +
NameLevel
{{logger.name | slice:0:140}} + + + + + +
+
diff --git a/jhipster/src/main/webapp/app/admin/logs/logs.component.ts b/jhipster/src/main/webapp/app/admin/logs/logs.component.ts new file mode 100644 index 0000000000..7655749018 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/logs/logs.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { Log } from './log.model'; +import { LogsService } from './logs.service'; + +@Component({ + selector: 'jhi-logs', + templateUrl: './logs.component.html', +}) +export class LogsComponent implements OnInit { + + loggers: Log[]; + filter: string; + orderProp: string; + reverse: boolean; + + constructor ( + private jhiLanguageService: JhiLanguageService, + private logsService: LogsService + ) { + this.filter = ''; + this.orderProp = 'name'; + this.reverse = false; + this.jhiLanguageService.setLocations(['logs']); + } + + ngOnInit() { + this.logsService.findAll().subscribe(loggers => this.loggers = loggers); + } + + changeLevel (name: string, level: string) { + let log = new Log(name, level); + this.logsService.changeLevel(log).subscribe(() => { + this.logsService.findAll().subscribe(loggers => this.loggers = loggers); + }); + } +} diff --git a/jhipster/src/main/webapp/app/admin/logs/logs.route.ts b/jhipster/src/main/webapp/app/admin/logs/logs.route.ts new file mode 100644 index 0000000000..1077f5da32 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/logs/logs.route.ts @@ -0,0 +1,12 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { LogsComponent } from './logs.component'; + +export const logsRoute: Route = { + path: 'logs', + component: LogsComponent, + data: { + pageTitle: 'logs.title' + } +}; diff --git a/jhipster/src/main/webapp/app/admin/logs/logs.service.ts b/jhipster/src/main/webapp/app/admin/logs/logs.service.ts new file mode 100644 index 0000000000..c937c10a42 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/logs/logs.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +import { Log } from './log.model'; + +@Injectable() +export class LogsService { + constructor(private http: Http) { } + + changeLevel(log: Log): Observable { + return this.http.put('management/logs', log); + } + + findAll(): Observable { + return this.http.get('management/logs').map((res: Response) => res.json()); + } +} diff --git a/jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.html b/jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.html new file mode 100644 index 0000000000..b8f0391acf --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.html @@ -0,0 +1,56 @@ + + + + diff --git a/jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.ts b/jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.ts new file mode 100644 index 0000000000..4a3034dd94 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/metrics/metrics-modal.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'jhi-metrics-modal', + templateUrl: './metrics-modal.component.html' +}) +export class JhiMetricsMonitoringModalComponent implements OnInit { + + threadDumpFilter: any; + threadDump: any; + threadDumpAll = 0; + threadDumpBlocked = 0; + threadDumpRunnable = 0; + threadDumpTimedWaiting = 0; + threadDumpWaiting = 0; + + constructor(public activeModal: NgbActiveModal) {} + + ngOnInit() { + this.threadDump.forEach((value) => { + if (value.threadState === 'RUNNABLE') { + this.threadDumpRunnable += 1; + } else if (value.threadState === 'WAITING') { + this.threadDumpWaiting += 1; + } else if (value.threadState === 'TIMED_WAITING') { + this.threadDumpTimedWaiting += 1; + } else if (value.threadState === 'BLOCKED') { + this.threadDumpBlocked += 1; + } + }); + + this.threadDumpAll = this.threadDumpRunnable + this.threadDumpWaiting + + this.threadDumpTimedWaiting + this.threadDumpBlocked; + } + + getBadgeClass (threadState) { + if (threadState === 'RUNNABLE') { + return 'badge-success'; + } else if (threadState === 'WAITING') { + return 'badge-info'; + } else if (threadState === 'TIMED_WAITING') { + return 'badge-warning'; + } else if (threadState === 'BLOCKED') { + return 'badge-danger'; + } + } +} diff --git a/jhipster/src/main/webapp/app/admin/metrics/metrics.component.html b/jhipster/src/main/webapp/app/admin/metrics/metrics.component.html new file mode 100644 index 0000000000..800ca21ef9 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/metrics/metrics.component.html @@ -0,0 +1,253 @@ +
+

+ Application Metrics + +

+ +

JVM Metrics

+
+
+ Memory +

Total Memory ({{metrics.gauges['jvm.memory.total.used'].value / 1000000 | number:'1.0-0'}}M / {{metrics.gauges['jvm.memory.total.max'].value / 1000000 | number:'1.0-0'}}M)

+ + {{metrics.gauges['jvm.memory.total.used'].value * 100 / metrics.gauges['jvm.memory.total.max'].value | number:'1.0-0'}}% + +

Heap Memory ({{metrics.gauges['jvm.memory.heap.used'].value / 1000000 | number:'1.0-0'}}M / {{metrics.gauges['jvm.memory.heap.max'].value / 1000000 | number:'1.0-0'}}M)

+ + {{metrics.gauges['jvm.memory.heap.used'].value * 100 / metrics.gauges['jvm.memory.heap.max'].value | number:'1.0-0'}}% + +

Non-Heap Memory ({{metrics.gauges['jvm.memory.non-heap.used'].value / 1000000 | number:'1.0-0'}}M / {{metrics.gauges['jvm.memory.non-heap.committed'].value / 1000000 | number:'1.0-0'}}M)

+ + {{metrics.gauges['jvm.memory.non-heap.used'].value * 100 / metrics.gauges['jvm.memory.non-heap.committed'].value | number:'1.0-0'}}% + +
+
+ Threads (Total: {{metrics.gauges['jvm.threads.count'].value}}) +

Runnable {{metrics.gauges['jvm.threads.runnable.count'].value}}

+ + {{metrics.gauges['jvm.threads.runnable.count'].value * 100 / metrics.gauges['jvm.threads.count'].value | number:'1.0-0'}}% + +

Timed Waiting ({{metrics.gauges['jvm.threads.timed_waiting.count'].value}})

+ + {{metrics.gauges['jvm.threads.timed_waiting.count'].value * 100 / metrics.gauges['jvm.threads.count'].value | number:'1.0-0'}}% + +

Waiting ({{metrics.gauges['jvm.threads.waiting.count'].value}})

+ + {{metrics.gauges['jvm.threads.waiting.count'].value * 100 / metrics.gauges['jvm.threads.count'].value | number:'1.0-0'}}% + +

Blocked ({{metrics.gauges['jvm.threads.blocked.count'].value}})

+ + {{metrics.gauges['jvm.threads.blocked.count'].value * 100 / metrics.gauges['jvm.threads.count'].value | number:'1.0-0'}}% + +
+
+ Garbage collections +
+
Mark Sweep count
+
{{metrics.gauges['jvm.garbage.PS-MarkSweep.count'].value}}
+
+
+
Mark Sweep time
+
{{metrics.gauges['jvm.garbage.PS-MarkSweep.time'].value}}ms
+
+
+
Scavenge count
+
{{metrics.gauges['jvm.garbage.PS-Scavenge.count'].value}}
+
+
+
Scavenge time
+
{{metrics.gauges['jvm.garbage.PS-Scavenge.time'].value}}ms
+
+
+
+
Updating...
+ +

HTTP requests (events per second)

+

+ Active requests {{metrics.counters['com.codahale.metrics.servlet.InstrumentedFilter.activeRequests'].count | number:'1.0-0'}} - Total requests {{metrics.timers['com.codahale.metrics.servlet.InstrumentedFilter.requests'].count | number:'1.0-0'}} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeCountMeanAverage (1 min)Average (5 min)Average (15 min)
OK + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].count}} + + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].mean_rate | number:'1.0-2'}} + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].m1_rate | number:'1.0-2'}} + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].m5_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].m15_rate | number:'1.0-2'}} +
Not Found + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].count}} + + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].mean_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].m1_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].m5_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].m15_rate | number:'1.0-2'}} +
Server error + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].count}} + + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].mean_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].m1_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].m5_rate | number:'1.0-2'}} + + {{metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].m15_rate | number:'1.0-2'}} +
+
+ +

Services statistics (time in millisecond)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Service nameCountMeanMinp50p75p95p99Max
{{entry.key}}{{entry.value.count}}{{entry.value.mean * 1000 | number:'1.0-0'}}{{entry.value.min * 1000 | number:'1.0-0'}}{{entry.value.p50 * 1000 | number:'1.0-0'}}{{entry.value.p75 * 1000 | number:'1.0-0'}}{{entry.value.p95 * 1000 | number:'1.0-0'}}{{entry.value.p99 * 1000 | number:'1.0-0'}}{{entry.value.max * 1000 | number:'1.0-0'}}
+
+ +

Cache statistics

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cache nameCache HitsCache MissesCache GetsCache PutsCache RemovalsCache EvictionsCache Hit %Cache Miss %Average get time (µs)Average put time (µs)Average remove time (µs)
{{entry.key}}{{metrics.gauges[entry.key + '.cache-hits'].value}}{{metrics.gauges[entry.key + '.cache-misses'].value}}{{metrics.gauges[entry.key + '.cache-gets'].value}}{{metrics.gauges[entry.key + '.cache-puts'].value}}{{metrics.gauges[entry.key + '.cache-removals'].value}}{{metrics.gauges[entry.key + '.cache-evictions'].value}}{{metrics.gauges[entry.key + '.cache-hit-percentage'].value}}{{metrics.gauges[entry.key + '.cache-miss-percentage'].value }}{{metrics.gauges[entry.key + '.average-get-time'].value | number : '1.2-2' }}{{metrics.gauges[entry.key + '.average-put-time'].value | number : '1.2-2'}}{{metrics.gauges[entry.key + '.average-remove-time'].value | number : '1.2-2' }}
+
+ +

DataSource statistics (time in millisecond)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Usage ({{metrics.gauges['HikariPool-1.pool.ActiveConnections'].value}} / {{metrics.gauges['HikariPool-1.pool.TotalConnections'].value}})CountMeanMinp50p75p95p99Max
+
+ + {{metrics.gauges['HikariPool-1.pool.ActiveConnections'].value * 100 / metrics.gauges['HikariPool-1.pool.TotalConnections'].value | number:'1.0-0'}}% + +
+
{{metrics.histograms['HikariPool-1.pool.Usage'].count}}{{metrics.histograms['HikariPool-1.pool.Usage'].mean | number:'1.0-2'}}{{metrics.histograms['HikariPool-1.pool.Usage'].min | number:'1.0-2'}}{{metrics.histograms['HikariPool-1.pool.Usage'].p50 | number:'1.0-2'}}{{metrics.histograms['HikariPool-1.pool.Usage'].p75 | number:'1.0-2'}}{{metrics.histograms['HikariPool-1.pool.Usage'].p95 | number:'1.0-2'}}{{metrics.histograms['HikariPool-1.pool.Usage'].p99 | number:'1.0-2'}}{{metrics.histograms['HikariPool-1.pool.Usage'].max | number:'1.0-2'}}
+
+
diff --git a/jhipster/src/main/webapp/app/admin/metrics/metrics.component.ts b/jhipster/src/main/webapp/app/admin/metrics/metrics.component.ts new file mode 100644 index 0000000000..21c39a3deb --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/metrics/metrics.component.ts @@ -0,0 +1,74 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { JhiMetricsMonitoringModalComponent } from './metrics-modal.component'; +import { JhiMetricsService } from './metrics.service'; + +@Component({ + selector: 'jhi-metrics', + templateUrl: './metrics.component.html', +}) +export class JhiMetricsMonitoringComponent implements OnInit { + metrics: any = {}; + cachesStats: any = {}; + servicesStats: any = {}; + updatingMetrics = true; + JCACHE_KEY: string ; + + constructor( + private jhiLanguageService: JhiLanguageService, + private modalService: NgbModal, + private metricsService: JhiMetricsService + ) { + this.JCACHE_KEY = 'jcache.statistics'; + this.jhiLanguageService.setLocations(['metrics']); + } + + ngOnInit() { + this.refresh(); + } + + refresh () { + this.updatingMetrics = true; + this.metricsService.getMetrics().subscribe((metrics) => { + this.metrics = metrics; + this.updatingMetrics = false; + this.servicesStats = {}; + this.cachesStats = {}; + Object.keys(metrics.timers).forEach((key) => { + let value = metrics.timers[key]; + if (key.indexOf('web.rest') !== -1 || key.indexOf('service') !== -1) { + this.servicesStats[key] = value; + } + }); + Object.keys(metrics.gauges).forEach((key) => { + if (key.indexOf('jcache.statistics') !== -1) { + let value = metrics.gauges[key].value; + // remove gets or puts + let index = key.lastIndexOf('.'); + let newKey = key.substr(0, index); + + // Keep the name of the domain + this.cachesStats[newKey] = { + 'name': this.JCACHE_KEY.length, + 'value': value + }; + } + }); + }); + } + + refreshThreadDumpData () { + this.metricsService.threadDump().subscribe((data) => { + const modalRef = this.modalService.open(JhiMetricsMonitoringModalComponent, { size: 'lg'}); + modalRef.componentInstance.threadDump = data; + modalRef.result.then((result) => { + // Left blank intentionally, nothing to do here + }, (reason) => { + // Left blank intentionally, nothing to do here + }); + }); + } + +} diff --git a/jhipster/src/main/webapp/app/admin/metrics/metrics.route.ts b/jhipster/src/main/webapp/app/admin/metrics/metrics.route.ts new file mode 100644 index 0000000000..cc13284dc8 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/metrics/metrics.route.ts @@ -0,0 +1,12 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { JhiMetricsMonitoringComponent } from './metrics.component'; + +export const metricsRoute: Route = { + path: 'jhi-metrics', + component: JhiMetricsMonitoringComponent, + data: { + pageTitle: 'metrics.title' + } +}; diff --git a/jhipster/src/main/webapp/app/admin/metrics/metrics.service.ts b/jhipster/src/main/webapp/app/admin/metrics/metrics.service.ts new file mode 100644 index 0000000000..2b31947339 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/metrics/metrics.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class JhiMetricsService { + + constructor (private http: Http) {} + + getMetrics(): Observable { + return this.http.get('management/metrics').map((res: Response) => res.json()); + } + + threadDump(): Observable { + return this.http.get('management/dump').map((res: Response) => res.json()); + } +} diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.html b/jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.html new file mode 100644 index 0000000000..452f1612c8 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.html @@ -0,0 +1,19 @@ +
+ + + +
diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.ts b/jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.ts new file mode 100644 index 0000000000..2c5c2a6a01 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management-delete-dialog.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, JhiLanguageService } from 'ng-jhipster'; + +import { User, UserService } from '../../shared'; +import { UserModalService } from './user-modal.service'; + +@Component({ + selector: 'jhi-user-mgmt-delete-dialog', + templateUrl: './user-management-delete-dialog.component.html' +}) +export class UserMgmtDeleteDialogComponent { + + user: User; + + constructor( + private jhiLanguageService: JhiLanguageService, + private userService: UserService, + public activeModal: NgbActiveModal, + private eventManager: EventManager + ) { + this.jhiLanguageService.setLocations(['user-management']); + } + + clear () { + this.activeModal.dismiss('cancel'); + } + + confirmDelete (login) { + this.userService.delete(login).subscribe(response => { + this.eventManager.broadcast({ name: 'userListModification', + content: 'Deleted a user'}); + this.activeModal.dismiss(true); + }); + } + +} + +@Component({ + selector: 'jhi-user-delete-dialog', + template: '' +}) +export class UserDeleteDialogComponent implements OnInit, OnDestroy { + + modalRef: NgbModalRef; + routeSub: any; + + constructor ( + private route: ActivatedRoute, + private userModalService: UserModalService + ) {} + + ngOnInit() { + this.routeSub = this.route.params.subscribe(params => { + this.modalRef = this.userModalService.open(UserMgmtDeleteDialogComponent, params['login']); + }); + } + + ngOnDestroy() { + this.routeSub.unsubscribe(); + } +} diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.html b/jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.html new file mode 100644 index 0000000000..d5deb8fa24 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.html @@ -0,0 +1,44 @@ +
+

+ User [{{user.login}}] +

+
+
Login
+
+ {{user.login}} + Deactivated + Activated +
+
First Name
+
{{user.firstName}}
+
Last Name
+
{{user.lastName}}
+
Email
+
{{user.email}}
+
Lang Key
+
{{user.langKey}}
+
Created By
+
{{user.createdBy}}
+
Created Date
+
{{user.createdDate | date:'dd/MM/yy HH:mm' }}
+
Last Modified By
+
{{user.lastModifiedBy}}
+
Last Modified Date
+
{{user.lastModifiedDate | date:'dd/MM/yy HH:mm'}}
+
Profiles
+
+
    +
  • + {{authority}} +
  • +
+
+
+ +
diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.ts b/jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.ts new file mode 100644 index 0000000000..564b1daf3d --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management-detail.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { User, UserService } from '../../shared'; + +@Component({ + selector: 'jhi-user-mgmt-detail', + templateUrl: './user-management-detail.component.html' +}) +export class UserMgmtDetailComponent implements OnInit, OnDestroy { + + user: User; + private subscription: any; + + constructor( + private jhiLanguageService: JhiLanguageService, + private userService: UserService, + private route: ActivatedRoute + ) { + this.jhiLanguageService.setLocations(['user-management']); + } + + ngOnInit() { + this.subscription = this.route.params.subscribe(params => { + this.load(params['login']); + }); + } + + load (login) { + this.userService.find(login).subscribe(user => { + this.user = user; + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + +} diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.html b/jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.html new file mode 100644 index 0000000000..6359527df7 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.html @@ -0,0 +1,112 @@ +
+ + + + +
diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.ts b/jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.ts new file mode 100644 index 0000000000..ebbf073df4 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management-dialog.component.ts @@ -0,0 +1,89 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, JhiLanguageService } from 'ng-jhipster'; + +import { UserModalService } from './user-modal.service'; +import { JhiLanguageHelper, User, UserService } from '../../shared'; + +@Component({ + selector: 'jhi-user-mgmt-dialog', + templateUrl: './user-management-dialog.component.html' +}) +export class UserMgmtDialogComponent implements OnInit { + + user: User; + languages: any[]; + authorities: any[]; + isSaving: Boolean; + + constructor ( + public activeModal: NgbActiveModal, + private languageHelper: JhiLanguageHelper, + private jhiLanguageService: JhiLanguageService, + private userService: UserService, + private eventManager: EventManager + ) {} + + ngOnInit() { + this.isSaving = false; + this.authorities = ['ROLE_USER', 'ROLE_ADMIN']; + this.languageHelper.getAll().then((languages) => { + this.languages = languages; + }); + this.jhiLanguageService.setLocations(['user-management']); + } + + clear() { + this.activeModal.dismiss('cancel'); + } + + save() { + this.isSaving = true; + if (this.user.id !== null) { + this.userService.update(this.user).subscribe(response => this.onSaveSuccess(response), () => this.onSaveError()); + } else { + this.userService.create(this.user).subscribe(response => this.onSaveSuccess(response), () => this.onSaveError()); + } + } + + private onSaveSuccess(result) { + this.eventManager.broadcast({ name: 'userListModification', content: 'OK' }); + this.isSaving = false; + this.activeModal.dismiss(result); + } + + private onSaveError() { + this.isSaving = false; + } +} + +@Component({ + selector: 'jhi-user-dialog', + template: '' +}) +export class UserDialogComponent implements OnInit, OnDestroy { + + modalRef: NgbModalRef; + routeSub: any; + + constructor ( + private route: ActivatedRoute, + private userModalService: UserModalService + ) {} + + ngOnInit() { + this.routeSub = this.route.params.subscribe(params => { + if ( params['login'] ) { + this.modalRef = this.userModalService.open(UserMgmtDialogComponent, params['login']); + } else { + this.modalRef = this.userModalService.open(UserMgmtDialogComponent); + } + }); + } + + ngOnDestroy() { + this.routeSub.unsubscribe(); + } +} diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management.component.html b/jhipster/src/main/webapp/app/admin/user-management/user-management.component.html new file mode 100644 index 0000000000..73fc18469a --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management.component.html @@ -0,0 +1,81 @@ +
+

+ Users + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDLogin Email Lang Key ProfilesCreated Date Last Modified By Last Modified Date
{{user.id}}{{user.login}}{{user.email}} + Deactivated + Activated + {{user.langKey}} +
+ {{ authority }} +
+
{{user.createdDate | date:'dd/MM/yy HH:mm'}}{{user.lastModifiedBy}}{{user.lastModifiedDate | date:'dd/MM/yy HH:mm'}} +
+ + + +
+
+
+
+
+ +
+
+ +
+
+
diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management.component.ts b/jhipster/src/main/webapp/app/admin/user-management/user-management.component.ts new file mode 100644 index 0000000000..213bb24923 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management.component.ts @@ -0,0 +1,131 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Response } from '@angular/http'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EventManager, PaginationUtil, ParseLinks, AlertService, JhiLanguageService } from 'ng-jhipster'; + +import { ITEMS_PER_PAGE, Principal, User, UserService } from '../../shared'; +import { PaginationConfig } from '../../blocks/config/uib-pagination.config'; + +@Component({ + selector: 'jhi-user-mgmt', + templateUrl: './user-management.component.html' +}) +export class UserMgmtComponent implements OnInit, OnDestroy { + + currentAccount: any; + users: User[]; + error: any; + success: any; + routeData: any; + links: any; + totalItems: any; + queryCount: any; + itemsPerPage: any; + page: any; + predicate: any; + previousPage: any; + reverse: any; + + constructor( + private jhiLanguageService: JhiLanguageService, + private userService: UserService, + private parseLinks: ParseLinks, + private alertService: AlertService, + private principal: Principal, + private eventManager: EventManager, private paginationUtil: PaginationUtil, + private paginationConfig: PaginationConfig, + private activatedRoute: ActivatedRoute, + private router: Router + ) { + this.itemsPerPage = ITEMS_PER_PAGE; + this.routeData = this.activatedRoute.data.subscribe(data => { + this.page = data['pagingParams'].page; + this.previousPage = data['pagingParams'].page; + this.reverse = data['pagingParams'].ascending; + this.predicate = data['pagingParams'].predicate; + }); + this.jhiLanguageService.setLocations(['user-management']); + } + + ngOnInit() { + this.principal.identity().then((account) => { + this.currentAccount = account; + this.loadAll(); + this.registerChangeInUsers(); + }); + } + + ngOnDestroy() { + this.routeData.unsubscribe(); + } + + registerChangeInUsers() { + this.eventManager.subscribe('userListModification', (response) => this.loadAll()); + } + + setActive (user, isActivated) { + user.activated = isActivated; + + this.userService.update(user).subscribe( + response => { + if (response.status === 200) { + this.error = null; + this.success = 'OK'; + this.loadAll(); + } else { + this.success = null; + this.error = 'ERROR'; + } + }); + } + + loadAll () { + this.userService.query({ + page: this.page - 1, + size: this.itemsPerPage, + sort: this.sort()}).subscribe( + (res: Response) => this.onSuccess(res.json(), res.headers), + (res: Response) => this.onError(res.json()) + ); + } + + trackIdentity (index, item: User) { + return item.id; + } + + sort () { + let result = [this.predicate + ',' + (this.reverse ? 'asc' : 'desc')]; + if (this.predicate !== 'id') { + result.push('id'); + } + return result; + } + + loadPage (page: number) { + if (page !== this.previousPage) { + this.previousPage = page; + this.transition(); + } + } + + transition () { + this.router.navigate(['/user-management'], { queryParams: + { + page: this.page, + sort: this.predicate + ',' + (this.reverse ? 'asc' : 'desc') + } + }); + this.loadAll(); + } + + private onSuccess(data, headers) { + this.links = this.parseLinks.parse(headers.get('link')); + this.totalItems = headers.get('X-Total-Count'); + this.queryCount = this.totalItems; + this.users = data; + } + + private onError(error) { + this.alertService.error(error.error, error.message, null); + } +} diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-management.route.ts b/jhipster/src/main/webapp/app/admin/user-management/user-management.route.ts new file mode 100644 index 0000000000..6aca0cdeb1 --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-management.route.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Routes, CanActivate } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { PaginationUtil } from 'ng-jhipster'; + +import { UserMgmtComponent } from './user-management.component'; +import { UserMgmtDetailComponent } from './user-management-detail.component'; +import { UserDialogComponent } from './user-management-dialog.component'; +import { UserDeleteDialogComponent } from './user-management-delete-dialog.component'; + +import { Principal } from '../../shared'; + + +@Injectable() +export class UserResolve implements CanActivate { + + constructor(private principal: Principal) { } + + canActivate() { + return this.principal.identity().then(account => this.principal.hasAnyAuthority(['ROLE_ADMIN'])); + } +} + +@Injectable() +export class UserResolvePagingParams implements Resolve { + + constructor(private paginationUtil: PaginationUtil) {} + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + let page = route.queryParams['page'] ? route.queryParams['page'] : '1'; + let sort = route.queryParams['sort'] ? route.queryParams['sort'] : 'id,asc'; + return { + page: this.paginationUtil.parsePage(page), + predicate: this.paginationUtil.parsePredicate(sort), + ascending: this.paginationUtil.parseAscending(sort) + }; + } +} + +export const userMgmtRoute: Routes = [ + { + path: 'user-management', + component: UserMgmtComponent, + resolve: { + 'pagingParams': UserResolvePagingParams + }, + data: { + pageTitle: 'userManagement.home.title' + } + }, + { + path: 'user-management/:login', + component: UserMgmtDetailComponent, + data: { + pageTitle: 'userManagement.home.title' + } + } +]; + +export const userDialogRoute: Routes = [ + { + path: 'user-management-new', + component: UserDialogComponent, + outlet: 'popup' + }, + { + path: 'user-management/:login/edit', + component: UserDialogComponent, + outlet: 'popup' + }, + { + path: 'user-management/:login/delete', + component: UserDeleteDialogComponent, + outlet: 'popup' + } +]; diff --git a/jhipster/src/main/webapp/app/admin/user-management/user-modal.service.ts b/jhipster/src/main/webapp/app/admin/user-management/user-modal.service.ts new file mode 100644 index 0000000000..006bc2a53c --- /dev/null +++ b/jhipster/src/main/webapp/app/admin/user-management/user-modal.service.ts @@ -0,0 +1,42 @@ +import { Injectable, Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; + +import { UserMgmtDialogComponent } from './user-management-dialog.component'; +import { User, UserService } from '../../shared'; + +@Injectable() +export class UserModalService { + private isOpen = false; + constructor ( + private modalService: NgbModal, + private router: Router, + private userService: UserService + ) {} + + open (component: Component, login?: string): NgbModalRef { + if (this.isOpen) { + return; + } + this.isOpen = true; + + if (login) { + this.userService.find(login).subscribe(user => this.userModalRef(component, user)); + } else { + return this.userModalRef(component, new User()); + } + } + + userModalRef(component: Component, user: User): NgbModalRef { + let modalRef = this.modalService.open(component, { size: 'lg', backdrop: 'static'}); + modalRef.componentInstance.user = user; + modalRef.result.then(result => { + this.router.navigate([{ outlets: { popup: null }}], { replaceUrl: true }); + this.isOpen = false; + }, (reason) => { + this.router.navigate([{ outlets: { popup: null }}], { replaceUrl: true }); + this.isOpen = false; + }); + return modalRef; + } +} diff --git a/jhipster/src/main/webapp/app/app.constants.ts b/jhipster/src/main/webapp/app/app.constants.ts new file mode 100644 index 0000000000..8f53b6f2e8 --- /dev/null +++ b/jhipster/src/main/webapp/app/app.constants.ts @@ -0,0 +1,7 @@ +// DO NOT EDIT THIS FILE, EDIT THE WEBPACK COMMON CONFIG INSTEAD, WHICH WILL MODIFY THIS FILE +let _VERSION = '0.0.0'; // This value will be overwritten by webpack +let _DEBUG_INFO_ENABLED = true; // This value will be overwritten by webpack +/* @toreplace VERSION */ +/* @toreplace DEBUG_INFO_ENABLED */ +export const VERSION = _VERSION; +export const DEBUG_INFO_ENABLED = _DEBUG_INFO_ENABLED; diff --git a/jhipster/src/main/webapp/app/app.main.ts b/jhipster/src/main/webapp/app/app.main.ts new file mode 100644 index 0000000000..301e5ffb81 --- /dev/null +++ b/jhipster/src/main/webapp/app/app.main.ts @@ -0,0 +1,11 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { ProdConfig } from './blocks/config/prod.config'; +import { BaeldungAppModule } from './app.module'; + +ProdConfig(); + +if (module['hot']) { + module['hot'].accept(); +} + +platformBrowserDynamic().bootstrapModule(BaeldungAppModule); diff --git a/jhipster/src/main/webapp/app/app.module.ts b/jhipster/src/main/webapp/app/app.module.ts new file mode 100644 index 0000000000..dc82c6cdce --- /dev/null +++ b/jhipster/src/main/webapp/app/app.module.ts @@ -0,0 +1,58 @@ +import './vendor.ts'; + +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserModule } from '@angular/platform-browser'; +import { Ng2Webstorage } from 'ng2-webstorage'; + +import { BaeldungSharedModule, UserRouteAccessService } from './shared'; +import { BaeldungHomeModule } from './home/home.module'; +import { BaeldungAdminModule } from './admin/admin.module'; +import { BaeldungAccountModule } from './account/account.module'; +import { BaeldungEntityModule } from './entities/entity.module'; + +import { LayoutRoutingModule } from './layouts'; +import { customHttpProvider } from './blocks/interceptor/http.provider'; +import { PaginationConfig } from './blocks/config/uib-pagination.config'; + +import { + JhiMainComponent, + NavbarComponent, + FooterComponent, + ProfileService, + PageRibbonComponent, + ActiveMenuDirective, + ErrorComponent +} from './layouts'; + + +@NgModule({ + imports: [ + BrowserModule, + LayoutRoutingModule, + Ng2Webstorage.forRoot({ prefix: 'jhi', separator: '-'}), + BaeldungSharedModule, + BaeldungHomeModule, + BaeldungAdminModule, + BaeldungAccountModule, + BaeldungEntityModule + ], + declarations: [ + JhiMainComponent, + NavbarComponent, + ErrorComponent, + PageRibbonComponent, + ActiveMenuDirective, + FooterComponent + ], + providers: [ + ProfileService, + { provide: Window, useValue: window }, + { provide: Document, useValue: document }, + customHttpProvider(), + PaginationConfig, + UserRouteAccessService + ], + bootstrap: [ JhiMainComponent ] +}) +export class BaeldungAppModule {} diff --git a/jhipster/src/main/webapp/app/app.route.ts b/jhipster/src/main/webapp/app/app.route.ts new file mode 100644 index 0000000000..bfddd9bb7e --- /dev/null +++ b/jhipster/src/main/webapp/app/app.route.ts @@ -0,0 +1,9 @@ +import { Route } from '@angular/router'; + +import { NavbarComponent } from './layouts'; + +export const navbarRoute: Route = { + path: '', + component: NavbarComponent, + outlet: 'navbar' + }; diff --git a/jhipster/src/main/webapp/app/blocks/config/prod.config.ts b/jhipster/src/main/webapp/app/blocks/config/prod.config.ts new file mode 100644 index 0000000000..cc0050de27 --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/config/prod.config.ts @@ -0,0 +1,9 @@ +import { enableProdMode } from '@angular/core'; +import { DEBUG_INFO_ENABLED } from '../../app.constants'; + +export function ProdConfig() { + // disable debug data on prod profile to improve performance + if (!DEBUG_INFO_ENABLED) { + enableProdMode(); + } +} diff --git a/jhipster/src/main/webapp/app/blocks/config/uib-pagination.config.ts b/jhipster/src/main/webapp/app/blocks/config/uib-pagination.config.ts new file mode 100644 index 0000000000..245e1f6bf1 --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/config/uib-pagination.config.ts @@ -0,0 +1,13 @@ +import { ITEMS_PER_PAGE } from '../../shared'; +import { Injectable } from '@angular/core'; +import { NgbPaginationConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Injectable() +export class PaginationConfig { + constructor(private config: NgbPaginationConfig) { + config.boundaryLinks = true; + config.maxSize = 5; + config.pageSize = ITEMS_PER_PAGE; + config.size = 'sm'; + } +} diff --git a/jhipster/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.ts b/jhipster/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.ts new file mode 100644 index 0000000000..49f517148c --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.ts @@ -0,0 +1,34 @@ +import { HttpInterceptor } from 'ng-jhipster'; +import { RequestOptionsArgs, Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import { Injector } from '@angular/core'; +import { AuthService } from '../../shared/auth/auth.service'; +import { Principal } from '../../shared/auth/principal.service'; +import { AuthServerProvider } from '../../shared/auth/auth-jwt.service'; + +export class AuthExpiredInterceptor extends HttpInterceptor { + + constructor(private injector: Injector) { + super(); + } + + requestIntercept(options?: RequestOptionsArgs): RequestOptionsArgs { + return options; + } + + responseIntercept(observable: Observable): Observable { + let self = this; + + return > observable.catch((error, source) => { + if (error.status === 401) { + let principal: Principal = self.injector.get(Principal); + + if (principal.isAuthenticated()) { + let auth: AuthService = self.injector.get(AuthService); + auth.authorize(true); + } + } + return Observable.throw(error); + }); + } +} diff --git a/jhipster/src/main/webapp/app/blocks/interceptor/auth.interceptor.ts b/jhipster/src/main/webapp/app/blocks/interceptor/auth.interceptor.ts new file mode 100644 index 0000000000..cf90284116 --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/interceptor/auth.interceptor.ts @@ -0,0 +1,27 @@ +import { Observable } from 'rxjs/Observable'; +import { RequestOptionsArgs, Response } from '@angular/http'; +import { LocalStorageService, SessionStorageService } from 'ng2-webstorage'; +import { HttpInterceptor } from 'ng-jhipster'; + +export class AuthInterceptor extends HttpInterceptor { + + constructor( + private localStorage: LocalStorageService, + private sessionStorage: SessionStorageService + ) { + super(); + } + + requestIntercept(options?: RequestOptionsArgs): RequestOptionsArgs { + let token = this.localStorage.retrieve('authenticationToken') || this.sessionStorage.retrieve('authenticationToken'); + if (!!token) { + options.headers.append('Authorization', 'Bearer ' + token); + } + return options; + } + + responseIntercept(observable: Observable): Observable { + return observable; // by pass + } + +} diff --git a/jhipster/src/main/webapp/app/blocks/interceptor/errorhandler.interceptor.ts b/jhipster/src/main/webapp/app/blocks/interceptor/errorhandler.interceptor.ts new file mode 100644 index 0000000000..6e22557d05 --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/interceptor/errorhandler.interceptor.ts @@ -0,0 +1,24 @@ +import { HttpInterceptor, EventManager } from 'ng-jhipster'; +import { RequestOptionsArgs, Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +export class ErrorHandlerInterceptor extends HttpInterceptor { + + constructor(private eventManager: EventManager) { + super(); + } + + requestIntercept(options?: RequestOptionsArgs): RequestOptionsArgs { + return options; + } + + responseIntercept(observable: Observable): Observable { + return > observable.catch(error => { + if (!(error.status === 401 && (error.text() === '' || + (error.json().path && error.json().path.indexOf('/api/account') === 0 )))) { + this.eventManager.broadcast( {name: 'baeldungApp.httpError', content: error}); + } + return Observable.throw(error); + }); + } +} diff --git a/jhipster/src/main/webapp/app/blocks/interceptor/http.provider.ts b/jhipster/src/main/webapp/app/blocks/interceptor/http.provider.ts new file mode 100644 index 0000000000..a9689662a3 --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/interceptor/http.provider.ts @@ -0,0 +1,45 @@ +import { Injector } from '@angular/core'; +import { Http, XHRBackend, RequestOptions } from '@angular/http'; +import { EventManager, InterceptableHttp } from 'ng-jhipster'; + +import { AuthInterceptor } from './auth.interceptor'; +import { LocalStorageService, SessionStorageService } from 'ng2-webstorage'; +import { AuthExpiredInterceptor } from './auth-expired.interceptor'; +import { ErrorHandlerInterceptor } from './errorhandler.interceptor'; +import { NotificationInterceptor } from './notification.interceptor'; + +export function interceptableFactory( + backend: XHRBackend, + defaultOptions: RequestOptions, + localStorage: LocalStorageService, + sessionStorage: SessionStorageService, + injector: Injector, + eventManager: EventManager +) { + return new InterceptableHttp( + backend, + defaultOptions, + [ + new AuthInterceptor(localStorage, sessionStorage), + new AuthExpiredInterceptor(injector), + // Other interceptors can be added here + new ErrorHandlerInterceptor(eventManager), + new NotificationInterceptor() + ] + ); +}; + +export function customHttpProvider() { + return { + provide: Http, + useFactory: interceptableFactory, + deps: [ + XHRBackend, + RequestOptions, + LocalStorageService, + SessionStorageService, + Injector, + EventManager + ] + }; +}; diff --git a/jhipster/src/main/webapp/app/blocks/interceptor/notification.interceptor.ts b/jhipster/src/main/webapp/app/blocks/interceptor/notification.interceptor.ts new file mode 100644 index 0000000000..ebee7688c5 --- /dev/null +++ b/jhipster/src/main/webapp/app/blocks/interceptor/notification.interceptor.ts @@ -0,0 +1,33 @@ +import { HttpInterceptor } from 'ng-jhipster'; +import { RequestOptionsArgs, Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +export class NotificationInterceptor extends HttpInterceptor { + + constructor() { + super(); + } + + requestIntercept(options?: RequestOptionsArgs): RequestOptionsArgs { + return options; + } + + responseIntercept(observable: Observable): Observable { + return > observable.catch((error) => { + let arr = Array.from(error.headers._headers); + let headers = []; + let i; + for (i = 0; i < arr.length; i++) { + if (arr[i][0].endsWith('app-alert') || arr[i][0].endsWith('app-params')) { + headers.push(arr[i][0]); + } + } + headers.sort(); + let alertKey = headers.length >= 1 ? error.headers.get(headers[0]) : null; + if (typeof alertKey === 'string') { + // AlertService.success(alertKey, { param: response.headers(headers[1])}); + } + return Observable.throw(error); + }); + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.html b/jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.html new file mode 100644 index 0000000000..63540ec6bd --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.html @@ -0,0 +1,19 @@ +
+ + + +
diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.ts b/jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.ts new file mode 100644 index 0000000000..29ef818ddf --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-delete-dialog.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, JhiLanguageService } from 'ng-jhipster'; + +import { Comment } from './comment.model'; +import { CommentPopupService } from './comment-popup.service'; +import { CommentService } from './comment.service'; + +@Component({ + selector: 'jhi-comment-delete-dialog', + templateUrl: './comment-delete-dialog.component.html' +}) +export class CommentDeleteDialogComponent { + + comment: Comment; + + constructor( + private jhiLanguageService: JhiLanguageService, + private commentService: CommentService, + public activeModal: NgbActiveModal, + private eventManager: EventManager + ) { + this.jhiLanguageService.setLocations(['comment']); + } + + clear () { + this.activeModal.dismiss('cancel'); + } + + confirmDelete (id: number) { + this.commentService.delete(id).subscribe(response => { + this.eventManager.broadcast({ + name: 'commentListModification', + content: 'Deleted an comment' + }); + this.activeModal.dismiss(true); + }); + } +} + +@Component({ + selector: 'jhi-comment-delete-popup', + template: '' +}) +export class CommentDeletePopupComponent implements OnInit, OnDestroy { + + modalRef: NgbModalRef; + routeSub: any; + + constructor ( + private route: ActivatedRoute, + private commentPopupService: CommentPopupService + ) {} + + ngOnInit() { + this.routeSub = this.route.params.subscribe(params => { + this.modalRef = this.commentPopupService + .open(CommentDeleteDialogComponent, params['id']); + }); + } + + ngOnDestroy() { + this.routeSub.unsubscribe(); + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-detail.component.html b/jhipster/src/main/webapp/app/entities/comment/comment-detail.component.html new file mode 100644 index 0000000000..add5b9e208 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-detail.component.html @@ -0,0 +1,35 @@ + +
+

Comment {{comment.id}}

+
+ +
+
Text
+
+ {{comment.text}} +
+
Creation Date
+
+ {{comment.creationDate | date:'mediumDate'}} +
+
Post
+
+ +
+
+ + + + +
diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-detail.component.ts b/jhipster/src/main/webapp/app/entities/comment/comment-detail.component.ts new file mode 100644 index 0000000000..c2bf7fce0f --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-detail.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { JhiLanguageService } from 'ng-jhipster'; +import { Comment } from './comment.model'; +import { CommentService } from './comment.service'; + +@Component({ + selector: 'jhi-comment-detail', + templateUrl: './comment-detail.component.html' +}) +export class CommentDetailComponent implements OnInit, OnDestroy { + + comment: Comment; + private subscription: any; + + constructor( + private jhiLanguageService: JhiLanguageService, + private commentService: CommentService, + private route: ActivatedRoute + ) { + this.jhiLanguageService.setLocations(['comment']); + } + + ngOnInit() { + this.subscription = this.route.params.subscribe(params => { + this.load(params['id']); + }); + } + + load (id) { + this.commentService.find(id).subscribe(comment => { + this.comment = comment; + }); + } + previousState() { + window.history.back(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.html b/jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.html new file mode 100644 index 0000000000..7e13c33177 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.html @@ -0,0 +1,76 @@ + + +
+ + + + +
diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.ts b/jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.ts new file mode 100644 index 0000000000..1a30305c96 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-dialog.component.ts @@ -0,0 +1,107 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Response } from '@angular/http'; + +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, AlertService, JhiLanguageService } from 'ng-jhipster'; + +import { Comment } from './comment.model'; +import { CommentPopupService } from './comment-popup.service'; +import { CommentService } from './comment.service'; +import { Post, PostService } from '../post'; +@Component({ + selector: 'jhi-comment-dialog', + templateUrl: './comment-dialog.component.html' +}) +export class CommentDialogComponent implements OnInit { + + comment: Comment; + authorities: any[]; + isSaving: boolean; + + posts: Post[]; + constructor( + public activeModal: NgbActiveModal, + private jhiLanguageService: JhiLanguageService, + private alertService: AlertService, + private commentService: CommentService, + private postService: PostService, + private eventManager: EventManager + ) { + this.jhiLanguageService.setLocations(['comment']); + } + + ngOnInit() { + this.isSaving = false; + this.authorities = ['ROLE_USER', 'ROLE_ADMIN']; + this.postService.query().subscribe( + (res: Response) => { this.posts = res.json(); }, (res: Response) => this.onError(res.json())); + } + clear () { + this.activeModal.dismiss('cancel'); + } + + save () { + this.isSaving = true; + if (this.comment.id !== undefined) { + this.commentService.update(this.comment) + .subscribe((res: Comment) => + this.onSaveSuccess(res), (res: Response) => this.onSaveError(res.json())); + } else { + this.commentService.create(this.comment) + .subscribe((res: Comment) => + this.onSaveSuccess(res), (res: Response) => this.onSaveError(res.json())); + } + } + + private onSaveSuccess (result: Comment) { + this.eventManager.broadcast({ name: 'commentListModification', content: 'OK'}); + this.isSaving = false; + this.activeModal.dismiss(result); + } + + private onSaveError (error) { + this.isSaving = false; + this.onError(error); + } + + private onError (error) { + this.alertService.error(error.message, null, null); + } + + trackPostById(index: number, item: Post) { + return item.id; + } +} + +@Component({ + selector: 'jhi-comment-popup', + template: '' +}) +export class CommentPopupComponent implements OnInit, OnDestroy { + + modalRef: NgbModalRef; + routeSub: any; + + constructor ( + private route: ActivatedRoute, + private commentPopupService: CommentPopupService + ) {} + + ngOnInit() { + this.routeSub = this.route.params.subscribe(params => { + if ( params['id'] ) { + this.modalRef = this.commentPopupService + .open(CommentDialogComponent, params['id']); + } else { + this.modalRef = this.commentPopupService + .open(CommentDialogComponent); + } + + }); + } + + ngOnDestroy() { + this.routeSub.unsubscribe(); + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment-popup.service.ts b/jhipster/src/main/webapp/app/entities/comment/comment-popup.service.ts new file mode 100644 index 0000000000..a1c0ce11bf --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment-popup.service.ts @@ -0,0 +1,50 @@ +import { Injectable, Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { Comment } from './comment.model'; +import { CommentService } from './comment.service'; +@Injectable() +export class CommentPopupService { + private isOpen = false; + constructor ( + private modalService: NgbModal, + private router: Router, + private commentService: CommentService + + ) {} + + open (component: Component, id?: number | any): NgbModalRef { + if (this.isOpen) { + return; + } + this.isOpen = true; + + if (id) { + this.commentService.find(id).subscribe(comment => { + if (comment.creationDate) { + comment.creationDate = { + year: comment.creationDate.getFullYear(), + month: comment.creationDate.getMonth() + 1, + day: comment.creationDate.getDate() + }; + } + this.commentModalRef(component, comment); + }); + } else { + return this.commentModalRef(component, new Comment()); + } + } + + commentModalRef(component: Component, comment: Comment): NgbModalRef { + let modalRef = this.modalService.open(component, { size: 'lg', backdrop: 'static'}); + modalRef.componentInstance.comment = comment; + modalRef.result.then(result => { + this.router.navigate([{ outlets: { popup: null }}], { replaceUrl: true }); + this.isOpen = false; + }, (reason) => { + this.router.navigate([{ outlets: { popup: null }}], { replaceUrl: true }); + this.isOpen = false; + }); + return modalRef; + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment.component.html b/jhipster/src/main/webapp/app/entities/comment/comment.component.html new file mode 100644 index 0000000000..1efd1ce379 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment.component.html @@ -0,0 +1,64 @@ +
+

+ Comments + +

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
ID Text Creation Date Post
{{comment.id}}{{comment.text}}{{comment.creationDate | date:'mediumDate'}} + + +
+ + + +
+
+
+
diff --git a/jhipster/src/main/webapp/app/entities/comment/comment.component.ts b/jhipster/src/main/webapp/app/entities/comment/comment.component.ts new file mode 100644 index 0000000000..4e009d4774 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Response } from '@angular/http'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs/Rx'; +import { EventManager, ParseLinks, PaginationUtil, JhiLanguageService, AlertService } from 'ng-jhipster'; + +import { Comment } from './comment.model'; +import { CommentService } from './comment.service'; +import { ITEMS_PER_PAGE, Principal } from '../../shared'; +import { PaginationConfig } from '../../blocks/config/uib-pagination.config'; + +@Component({ + selector: 'jhi-comment', + templateUrl: './comment.component.html' +}) +export class CommentComponent implements OnInit, OnDestroy { + + comments: Comment[]; + currentAccount: any; + eventSubscriber: Subscription; + itemsPerPage: number; + links: any; + page: any; + predicate: any; + queryCount: any; + reverse: any; + totalItems: number; + + constructor( + private jhiLanguageService: JhiLanguageService, + private commentService: CommentService, + private alertService: AlertService, + private eventManager: EventManager, + private parseLinks: ParseLinks, + private principal: Principal + ) { + this.comments = []; + this.itemsPerPage = ITEMS_PER_PAGE; + this.page = 0; + this.links = { + last: 0 + }; + this.predicate = 'id'; + this.reverse = true; + this.jhiLanguageService.setLocations(['comment']); + } + + loadAll () { + this.commentService.query({ + page: this.page, + size: this.itemsPerPage, + sort: this.sort() + }).subscribe( + (res: Response) => this.onSuccess(res.json(), res.headers), + (res: Response) => this.onError(res.json()) + ); + } + + reset () { + this.page = 0; + this.comments = []; + this.loadAll(); + } + + loadPage(page) { + this.page = page; + this.loadAll(); + } + ngOnInit() { + this.loadAll(); + this.principal.identity().then((account) => { + this.currentAccount = account; + }); + this.registerChangeInComments(); + } + + ngOnDestroy() { + this.eventManager.destroy(this.eventSubscriber); + } + + trackId (index: number, item: Comment) { + return item.id; + } + + + + registerChangeInComments() { + this.eventSubscriber = this.eventManager.subscribe('commentListModification', (response) => this.reset()); + } + + sort () { + let result = [this.predicate + ',' + (this.reverse ? 'asc' : 'desc')]; + if (this.predicate !== 'id') { + result.push('id'); + } + return result; + } + + private onSuccess(data, headers) { + this.links = this.parseLinks.parse(headers.get('link')); + this.totalItems = headers.get('X-Total-Count'); + for (let i = 0; i < data.length; i++) { + this.comments.push(data[i]); + } + } + + private onError (error) { + this.alertService.error(error.message, null, null); + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment.model.ts b/jhipster/src/main/webapp/app/entities/comment/comment.model.ts new file mode 100644 index 0000000000..66df7d0b34 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment.model.ts @@ -0,0 +1,10 @@ +import { Post } from '../post'; +export class Comment { + constructor( + public id?: number, + public text?: string, + public creationDate?: any, + public post?: Post, + ) { + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment.module.ts b/jhipster/src/main/webapp/app/entities/comment/comment.module.ts new file mode 100644 index 0000000000..1f3167274e --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment.module.ts @@ -0,0 +1,50 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { BaeldungSharedModule } from '../../shared'; + +import { + CommentService, + CommentPopupService, + CommentComponent, + CommentDetailComponent, + CommentDialogComponent, + CommentPopupComponent, + CommentDeletePopupComponent, + CommentDeleteDialogComponent, + commentRoute, + commentPopupRoute, +} from './'; + +let ENTITY_STATES = [ + ...commentRoute, + ...commentPopupRoute, +]; + +@NgModule({ + imports: [ + BaeldungSharedModule, + RouterModule.forRoot(ENTITY_STATES, { useHash: true }) + ], + declarations: [ + CommentComponent, + CommentDetailComponent, + CommentDialogComponent, + CommentDeleteDialogComponent, + CommentPopupComponent, + CommentDeletePopupComponent, + ], + entryComponents: [ + CommentComponent, + CommentDialogComponent, + CommentPopupComponent, + CommentDeleteDialogComponent, + CommentDeletePopupComponent, + ], + providers: [ + CommentService, + CommentPopupService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BaeldungCommentModule {} diff --git a/jhipster/src/main/webapp/app/entities/comment/comment.route.ts b/jhipster/src/main/webapp/app/entities/comment/comment.route.ts new file mode 100644 index 0000000000..bb0e5e4141 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment.route.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Routes, CanActivate } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { PaginationUtil } from 'ng-jhipster'; + +import { CommentComponent } from './comment.component'; +import { CommentDetailComponent } from './comment-detail.component'; +import { CommentPopupComponent } from './comment-dialog.component'; +import { CommentDeletePopupComponent } from './comment-delete-dialog.component'; + +import { Principal } from '../../shared'; + + +export const commentRoute: Routes = [ + { + path: 'comment', + component: CommentComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.comment.home.title' + } + }, { + path: 'comment/:id', + component: CommentDetailComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.comment.home.title' + } + } +]; + +export const commentPopupRoute: Routes = [ + { + path: 'comment-new', + component: CommentPopupComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.comment.home.title' + }, + outlet: 'popup' + }, + { + path: 'comment/:id/edit', + component: CommentPopupComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.comment.home.title' + }, + outlet: 'popup' + }, + { + path: 'comment/:id/delete', + component: CommentDeletePopupComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.comment.home.title' + }, + outlet: 'popup' + } +]; diff --git a/jhipster/src/main/webapp/app/entities/comment/comment.service.ts b/jhipster/src/main/webapp/app/entities/comment/comment.service.ts new file mode 100644 index 0000000000..b6121b3b10 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/comment.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, URLSearchParams, BaseRequestOptions } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +import { Comment } from './comment.model'; +import { DateUtils } from 'ng-jhipster'; +@Injectable() +export class CommentService { + + private resourceUrl = 'api/comments'; + + constructor(private http: Http, private dateUtils: DateUtils) { } + + create(comment: Comment): Observable { + let copy: Comment = Object.assign({}, comment); + copy.creationDate = this.dateUtils + .convertLocalDateToServer(comment.creationDate); + return this.http.post(this.resourceUrl, copy).map((res: Response) => { + return res.json(); + }); + } + + update(comment: Comment): Observable { + let copy: Comment = Object.assign({}, comment); + copy.creationDate = this.dateUtils + .convertLocalDateToServer(comment.creationDate); + return this.http.put(this.resourceUrl, copy).map((res: Response) => { + return res.json(); + }); + } + + find(id: number): Observable { + return this.http.get(`${this.resourceUrl}/${id}`).map((res: Response) => { + let jsonResponse = res.json(); + jsonResponse.creationDate = this.dateUtils + .convertLocalDateFromServer(jsonResponse.creationDate); + return jsonResponse; + }); + } + + query(req?: any): Observable { + let options = this.createRequestOption(req); + return this.http.get(this.resourceUrl, options) + .map((res: any) => this.convertResponse(res)) + ; + } + + delete(id: number): Observable { + return this.http.delete(`${this.resourceUrl}/${id}`); + } + + + private convertResponse(res: any): any { + let jsonResponse = res.json(); + for (let i = 0; i < jsonResponse.length; i++) { + jsonResponse[i].creationDate = this.dateUtils + .convertLocalDateFromServer(jsonResponse[i].creationDate); + } + res._body = jsonResponse; + return res; + } + + private createRequestOption(req?: any): BaseRequestOptions { + let options: BaseRequestOptions = new BaseRequestOptions(); + if (req) { + let params: URLSearchParams = new URLSearchParams(); + params.set('page', req.page); + params.set('size', req.size); + if (req.sort) { + params.paramsMap.set('sort', req.sort); + } + params.set('query', req.query); + + options.search = params; + } + return options; + } +} diff --git a/jhipster/src/main/webapp/app/entities/comment/index.ts b/jhipster/src/main/webapp/app/entities/comment/index.ts new file mode 100644 index 0000000000..5e54fb6099 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/comment/index.ts @@ -0,0 +1,8 @@ +export * from './comment.model'; +export * from './comment-popup.service'; +export * from './comment.service'; +export * from './comment-dialog.component'; +export * from './comment-delete-dialog.component'; +export * from './comment-detail.component'; +export * from './comment.component'; +export * from './comment.route'; diff --git a/jhipster/src/main/webapp/app/entities/entity.module.ts b/jhipster/src/main/webapp/app/entities/entity.module.ts new file mode 100644 index 0000000000..ba044e1902 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/entity.module.ts @@ -0,0 +1,18 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + +import { BaeldungPostModule } from './post/post.module'; +import { BaeldungCommentModule } from './comment/comment.module'; +/* jhipster-needle-add-entity-module-import - JHipster will add entity modules imports here */ + +@NgModule({ + imports: [ + BaeldungPostModule, + BaeldungCommentModule, + /* jhipster-needle-add-entity-module - JHipster will add entity modules here */ + ], + declarations: [], + entryComponents: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BaeldungEntityModule {} diff --git a/jhipster/src/main/webapp/app/entities/post/index.ts b/jhipster/src/main/webapp/app/entities/post/index.ts new file mode 100644 index 0000000000..9375e11bd8 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/index.ts @@ -0,0 +1,8 @@ +export * from './post.model'; +export * from './post-popup.service'; +export * from './post.service'; +export * from './post-dialog.component'; +export * from './post-delete-dialog.component'; +export * from './post-detail.component'; +export * from './post.component'; +export * from './post.route'; diff --git a/jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.html b/jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.html new file mode 100644 index 0000000000..901fc1968e --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.html @@ -0,0 +1,19 @@ +
+ + + +
diff --git a/jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.ts b/jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.ts new file mode 100644 index 0000000000..5e1413fd95 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-delete-dialog.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, JhiLanguageService } from 'ng-jhipster'; + +import { Post } from './post.model'; +import { PostPopupService } from './post-popup.service'; +import { PostService } from './post.service'; + +@Component({ + selector: 'jhi-post-delete-dialog', + templateUrl: './post-delete-dialog.component.html' +}) +export class PostDeleteDialogComponent { + + post: Post; + + constructor( + private jhiLanguageService: JhiLanguageService, + private postService: PostService, + public activeModal: NgbActiveModal, + private eventManager: EventManager + ) { + this.jhiLanguageService.setLocations(['post']); + } + + clear () { + this.activeModal.dismiss('cancel'); + } + + confirmDelete (id: number) { + this.postService.delete(id).subscribe(response => { + this.eventManager.broadcast({ + name: 'postListModification', + content: 'Deleted an post' + }); + this.activeModal.dismiss(true); + }); + } +} + +@Component({ + selector: 'jhi-post-delete-popup', + template: '' +}) +export class PostDeletePopupComponent implements OnInit, OnDestroy { + + modalRef: NgbModalRef; + routeSub: any; + + constructor ( + private route: ActivatedRoute, + private postPopupService: PostPopupService + ) {} + + ngOnInit() { + this.routeSub = this.route.params.subscribe(params => { + this.modalRef = this.postPopupService + .open(PostDeleteDialogComponent, params['id']); + }); + } + + ngOnDestroy() { + this.routeSub.unsubscribe(); + } +} diff --git a/jhipster/src/main/webapp/app/entities/post/post-detail.component.html b/jhipster/src/main/webapp/app/entities/post/post-detail.component.html new file mode 100644 index 0000000000..8edcbee375 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-detail.component.html @@ -0,0 +1,37 @@ + +
+

Post {{post.id}}

+
+ +
+
Title
+
+ {{post.title}} +
+
Content
+
+ {{post.content}} +
+
Creation Date
+
+ {{post.creationDate | date:'mediumDate'}} +
+
Creator
+
+ {{post.creator?.login}} +
+
+ + + + +
diff --git a/jhipster/src/main/webapp/app/entities/post/post-detail.component.ts b/jhipster/src/main/webapp/app/entities/post/post-detail.component.ts new file mode 100644 index 0000000000..679592f3b8 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-detail.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { JhiLanguageService } from 'ng-jhipster'; +import { Post } from './post.model'; +import { PostService } from './post.service'; + +@Component({ + selector: 'jhi-post-detail', + templateUrl: './post-detail.component.html' +}) +export class PostDetailComponent implements OnInit, OnDestroy { + + post: Post; + private subscription: any; + + constructor( + private jhiLanguageService: JhiLanguageService, + private postService: PostService, + private route: ActivatedRoute + ) { + this.jhiLanguageService.setLocations(['post']); + } + + ngOnInit() { + this.subscription = this.route.params.subscribe(params => { + this.load(params['id']); + }); + } + + load (id) { + this.postService.find(id).subscribe(post => { + this.post = post; + }); + } + previousState() { + window.history.back(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + +} diff --git a/jhipster/src/main/webapp/app/entities/post/post-dialog.component.html b/jhipster/src/main/webapp/app/entities/post/post-dialog.component.html new file mode 100644 index 0000000000..2216dcd451 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-dialog.component.html @@ -0,0 +1,96 @@ + + +
+ + + + +
diff --git a/jhipster/src/main/webapp/app/entities/post/post-dialog.component.ts b/jhipster/src/main/webapp/app/entities/post/post-dialog.component.ts new file mode 100644 index 0000000000..803b76fc78 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-dialog.component.ts @@ -0,0 +1,107 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Response } from '@angular/http'; + +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, AlertService, JhiLanguageService } from 'ng-jhipster'; + +import { Post } from './post.model'; +import { PostPopupService } from './post-popup.service'; +import { PostService } from './post.service'; +import { User, UserService } from '../../shared'; +@Component({ + selector: 'jhi-post-dialog', + templateUrl: './post-dialog.component.html' +}) +export class PostDialogComponent implements OnInit { + + post: Post; + authorities: any[]; + isSaving: boolean; + + users: User[]; + constructor( + public activeModal: NgbActiveModal, + private jhiLanguageService: JhiLanguageService, + private alertService: AlertService, + private postService: PostService, + private userService: UserService, + private eventManager: EventManager + ) { + this.jhiLanguageService.setLocations(['post']); + } + + ngOnInit() { + this.isSaving = false; + this.authorities = ['ROLE_USER', 'ROLE_ADMIN']; + this.userService.query().subscribe( + (res: Response) => { this.users = res.json(); }, (res: Response) => this.onError(res.json())); + } + clear () { + this.activeModal.dismiss('cancel'); + } + + save () { + this.isSaving = true; + if (this.post.id !== undefined) { + this.postService.update(this.post) + .subscribe((res: Post) => + this.onSaveSuccess(res), (res: Response) => this.onSaveError(res.json())); + } else { + this.postService.create(this.post) + .subscribe((res: Post) => + this.onSaveSuccess(res), (res: Response) => this.onSaveError(res.json())); + } + } + + private onSaveSuccess (result: Post) { + this.eventManager.broadcast({ name: 'postListModification', content: 'OK'}); + this.isSaving = false; + this.activeModal.dismiss(result); + } + + private onSaveError (error) { + this.isSaving = false; + this.onError(error); + } + + private onError (error) { + this.alertService.error(error.message, null, null); + } + + trackUserById(index: number, item: User) { + return item.id; + } +} + +@Component({ + selector: 'jhi-post-popup', + template: '' +}) +export class PostPopupComponent implements OnInit, OnDestroy { + + modalRef: NgbModalRef; + routeSub: any; + + constructor ( + private route: ActivatedRoute, + private postPopupService: PostPopupService + ) {} + + ngOnInit() { + this.routeSub = this.route.params.subscribe(params => { + if ( params['id'] ) { + this.modalRef = this.postPopupService + .open(PostDialogComponent, params['id']); + } else { + this.modalRef = this.postPopupService + .open(PostDialogComponent); + } + + }); + } + + ngOnDestroy() { + this.routeSub.unsubscribe(); + } +} diff --git a/jhipster/src/main/webapp/app/entities/post/post-popup.service.ts b/jhipster/src/main/webapp/app/entities/post/post-popup.service.ts new file mode 100644 index 0000000000..42ea731e07 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post-popup.service.ts @@ -0,0 +1,50 @@ +import { Injectable, Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { Post } from './post.model'; +import { PostService } from './post.service'; +@Injectable() +export class PostPopupService { + private isOpen = false; + constructor ( + private modalService: NgbModal, + private router: Router, + private postService: PostService + + ) {} + + open (component: Component, id?: number | any): NgbModalRef { + if (this.isOpen) { + return; + } + this.isOpen = true; + + if (id) { + this.postService.find(id).subscribe(post => { + if (post.creationDate) { + post.creationDate = { + year: post.creationDate.getFullYear(), + month: post.creationDate.getMonth() + 1, + day: post.creationDate.getDate() + }; + } + this.postModalRef(component, post); + }); + } else { + return this.postModalRef(component, new Post()); + } + } + + postModalRef(component: Component, post: Post): NgbModalRef { + let modalRef = this.modalService.open(component, { size: 'lg', backdrop: 'static'}); + modalRef.componentInstance.post = post; + modalRef.result.then(result => { + this.router.navigate([{ outlets: { popup: null }}], { replaceUrl: true }); + this.isOpen = false; + }, (reason) => { + this.router.navigate([{ outlets: { popup: null }}], { replaceUrl: true }); + this.isOpen = false; + }); + return modalRef; + } +} diff --git a/jhipster/src/main/webapp/app/entities/post/post.component.html b/jhipster/src/main/webapp/app/entities/post/post.component.html new file mode 100644 index 0000000000..cf17c4e6dc --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post.component.html @@ -0,0 +1,64 @@ +
+

+ Posts + +

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
ID Title Content Creation Date Creator
{{post.id}}{{post.title}}{{post.content}}{{post.creationDate | date:'mediumDate'}} + {{post.creator?.login}} + +
+ + + +
+
+
+
diff --git a/jhipster/src/main/webapp/app/entities/post/post.component.ts b/jhipster/src/main/webapp/app/entities/post/post.component.ts new file mode 100644 index 0000000000..e8401a48c7 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Response } from '@angular/http'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs/Rx'; +import { EventManager, ParseLinks, PaginationUtil, JhiLanguageService, AlertService } from 'ng-jhipster'; + +import { Post } from './post.model'; +import { PostService } from './post.service'; +import { ITEMS_PER_PAGE, Principal } from '../../shared'; +import { PaginationConfig } from '../../blocks/config/uib-pagination.config'; + +@Component({ + selector: 'jhi-post', + templateUrl: './post.component.html' +}) +export class PostComponent implements OnInit, OnDestroy { + + posts: Post[]; + currentAccount: any; + eventSubscriber: Subscription; + itemsPerPage: number; + links: any; + page: any; + predicate: any; + queryCount: any; + reverse: any; + totalItems: number; + + constructor( + private jhiLanguageService: JhiLanguageService, + private postService: PostService, + private alertService: AlertService, + private eventManager: EventManager, + private parseLinks: ParseLinks, + private principal: Principal + ) { + this.posts = []; + this.itemsPerPage = ITEMS_PER_PAGE; + this.page = 0; + this.links = { + last: 0 + }; + this.predicate = 'id'; + this.reverse = true; + this.jhiLanguageService.setLocations(['post']); + } + + loadAll () { + this.postService.query({ + page: this.page, + size: this.itemsPerPage, + sort: this.sort() + }).subscribe( + (res: Response) => this.onSuccess(res.json(), res.headers), + (res: Response) => this.onError(res.json()) + ); + } + + reset () { + this.page = 0; + this.posts = []; + this.loadAll(); + } + + loadPage(page) { + this.page = page; + this.loadAll(); + } + ngOnInit() { + this.loadAll(); + this.principal.identity().then((account) => { + this.currentAccount = account; + }); + this.registerChangeInPosts(); + } + + ngOnDestroy() { + this.eventManager.destroy(this.eventSubscriber); + } + + trackId (index: number, item: Post) { + return item.id; + } + + + + registerChangeInPosts() { + this.eventSubscriber = this.eventManager.subscribe('postListModification', (response) => this.reset()); + } + + sort () { + let result = [this.predicate + ',' + (this.reverse ? 'asc' : 'desc')]; + if (this.predicate !== 'id') { + result.push('id'); + } + return result; + } + + private onSuccess(data, headers) { + this.links = this.parseLinks.parse(headers.get('link')); + this.totalItems = headers.get('X-Total-Count'); + for (let i = 0; i < data.length; i++) { + this.posts.push(data[i]); + } + } + + private onError (error) { + this.alertService.error(error.message, null, null); + } +} diff --git a/jhipster/src/main/webapp/app/entities/post/post.model.ts b/jhipster/src/main/webapp/app/entities/post/post.model.ts new file mode 100644 index 0000000000..454f7e75f8 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post.model.ts @@ -0,0 +1,11 @@ +import { User } from '../../shared'; +export class Post { + constructor( + public id?: number, + public title?: string, + public content?: string, + public creationDate?: any, + public creator?: User, + ) { + } +} diff --git a/jhipster/src/main/webapp/app/entities/post/post.module.ts b/jhipster/src/main/webapp/app/entities/post/post.module.ts new file mode 100644 index 0000000000..53ba982c02 --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post.module.ts @@ -0,0 +1,52 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { BaeldungSharedModule } from '../../shared'; +import { BaeldungAdminModule } from '../../admin/admin.module'; + +import { + PostService, + PostPopupService, + PostComponent, + PostDetailComponent, + PostDialogComponent, + PostPopupComponent, + PostDeletePopupComponent, + PostDeleteDialogComponent, + postRoute, + postPopupRoute, +} from './'; + +let ENTITY_STATES = [ + ...postRoute, + ...postPopupRoute, +]; + +@NgModule({ + imports: [ + BaeldungSharedModule, + BaeldungAdminModule, + RouterModule.forRoot(ENTITY_STATES, { useHash: true }) + ], + declarations: [ + PostComponent, + PostDetailComponent, + PostDialogComponent, + PostDeleteDialogComponent, + PostPopupComponent, + PostDeletePopupComponent, + ], + entryComponents: [ + PostComponent, + PostDialogComponent, + PostPopupComponent, + PostDeleteDialogComponent, + PostDeletePopupComponent, + ], + providers: [ + PostService, + PostPopupService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BaeldungPostModule {} diff --git a/jhipster/src/main/webapp/app/entities/post/post.route.ts b/jhipster/src/main/webapp/app/entities/post/post.route.ts new file mode 100644 index 0000000000..27b23bdc0d --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post.route.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Routes, CanActivate } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { PaginationUtil } from 'ng-jhipster'; + +import { PostComponent } from './post.component'; +import { PostDetailComponent } from './post-detail.component'; +import { PostPopupComponent } from './post-dialog.component'; +import { PostDeletePopupComponent } from './post-delete-dialog.component'; + +import { Principal } from '../../shared'; + + +export const postRoute: Routes = [ + { + path: 'post', + component: PostComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.post.home.title' + } + }, { + path: 'post/:id', + component: PostDetailComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.post.home.title' + } + } +]; + +export const postPopupRoute: Routes = [ + { + path: 'post-new', + component: PostPopupComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.post.home.title' + }, + outlet: 'popup' + }, + { + path: 'post/:id/edit', + component: PostPopupComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.post.home.title' + }, + outlet: 'popup' + }, + { + path: 'post/:id/delete', + component: PostDeletePopupComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'baeldungApp.post.home.title' + }, + outlet: 'popup' + } +]; diff --git a/jhipster/src/main/webapp/app/entities/post/post.service.ts b/jhipster/src/main/webapp/app/entities/post/post.service.ts new file mode 100644 index 0000000000..7f7ee969fe --- /dev/null +++ b/jhipster/src/main/webapp/app/entities/post/post.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, URLSearchParams, BaseRequestOptions } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +import { Post } from './post.model'; +import { DateUtils } from 'ng-jhipster'; +@Injectable() +export class PostService { + + private resourceUrl = 'api/posts'; + + constructor(private http: Http, private dateUtils: DateUtils) { } + + create(post: Post): Observable { + let copy: Post = Object.assign({}, post); + copy.creationDate = this.dateUtils + .convertLocalDateToServer(post.creationDate); + return this.http.post(this.resourceUrl, copy).map((res: Response) => { + return res.json(); + }); + } + + update(post: Post): Observable { + let copy: Post = Object.assign({}, post); + copy.creationDate = this.dateUtils + .convertLocalDateToServer(post.creationDate); + return this.http.put(this.resourceUrl, copy).map((res: Response) => { + return res.json(); + }); + } + + find(id: number): Observable { + return this.http.get(`${this.resourceUrl}/${id}`).map((res: Response) => { + let jsonResponse = res.json(); + jsonResponse.creationDate = this.dateUtils + .convertLocalDateFromServer(jsonResponse.creationDate); + return jsonResponse; + }); + } + + query(req?: any): Observable { + let options = this.createRequestOption(req); + return this.http.get(this.resourceUrl, options) + .map((res: any) => this.convertResponse(res)) + ; + } + + delete(id: number): Observable { + return this.http.delete(`${this.resourceUrl}/${id}`); + } + + + private convertResponse(res: any): any { + let jsonResponse = res.json(); + for (let i = 0; i < jsonResponse.length; i++) { + jsonResponse[i].creationDate = this.dateUtils + .convertLocalDateFromServer(jsonResponse[i].creationDate); + } + res._body = jsonResponse; + return res; + } + + private createRequestOption(req?: any): BaseRequestOptions { + let options: BaseRequestOptions = new BaseRequestOptions(); + if (req) { + let params: URLSearchParams = new URLSearchParams(); + params.set('page', req.page); + params.set('size', req.size); + if (req.sort) { + params.paramsMap.set('sort', req.sort); + } + params.set('query', req.query); + + options.search = params; + } + return options; + } +} diff --git a/jhipster/src/main/webapp/app/home/home.component.html b/jhipster/src/main/webapp/app/home/home.component.html new file mode 100644 index 0000000000..4c0197228c --- /dev/null +++ b/jhipster/src/main/webapp/app/home/home.component.html @@ -0,0 +1,42 @@ +
+
+ +
+
+

Welcome, Java Hipster!

+

This is your homepage

+ +
+
+ You are logged in as user "{{account.login}}". +
+ +
+ If you want to + sign in, you can try the default accounts:
- Administrator (login="admin" and password="admin")
- User (login="user" and password="user").
+
+ +
+ You don't have an account yet? + Register a new account +
+
+ +

+ If you have any question on JHipster: +

+ + + +

+ If you like JHipster, don't forget to give us a star on Github! +

+
+
diff --git a/jhipster/src/main/webapp/app/home/home.component.ts b/jhipster/src/main/webapp/app/home/home.component.ts new file mode 100644 index 0000000000..b16838377e --- /dev/null +++ b/jhipster/src/main/webapp/app/home/home.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { EventManager, JhiLanguageService } from 'ng-jhipster'; + +import { Account, LoginModalService, Principal } from '../shared'; + +@Component({ + selector: 'jhi-home', + templateUrl: './home.component.html', + styleUrls: [ + 'home.scss' + ] + +}) +export class HomeComponent implements OnInit { + account: Account; + modalRef: NgbModalRef; + + constructor( + private jhiLanguageService: JhiLanguageService, + private principal: Principal, + private loginModalService: LoginModalService, + private eventManager: EventManager + ) { + this.jhiLanguageService.setLocations(['home']); + } + + ngOnInit() { + this.principal.identity().then((account) => { + this.account = account; + }); + this.registerAuthenticationSuccess(); + } + + registerAuthenticationSuccess() { + this.eventManager.subscribe('authenticationSuccess', (message) => { + this.principal.identity().then((account) => { + this.account = account; + }); + }); + } + + isAuthenticated() { + return this.principal.isAuthenticated(); + } + + login() { + this.modalRef = this.loginModalService.open(); + } +} diff --git a/jhipster/src/main/webapp/app/home/home.module.ts b/jhipster/src/main/webapp/app/home/home.module.ts new file mode 100644 index 0000000000..172c605249 --- /dev/null +++ b/jhipster/src/main/webapp/app/home/home.module.ts @@ -0,0 +1,23 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { BaeldungSharedModule } from '../shared'; + +import { HOME_ROUTE, HomeComponent } from './'; + + +@NgModule({ + imports: [ + BaeldungSharedModule, + RouterModule.forRoot([ HOME_ROUTE ], { useHash: true }) + ], + declarations: [ + HomeComponent, + ], + entryComponents: [ + ], + providers: [ + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BaeldungHomeModule {} diff --git a/jhipster/src/main/webapp/app/home/home.route.ts b/jhipster/src/main/webapp/app/home/home.route.ts new file mode 100644 index 0000000000..cba14bf0c6 --- /dev/null +++ b/jhipster/src/main/webapp/app/home/home.route.ts @@ -0,0 +1,14 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from '../shared'; +import { HomeComponent } from './'; + +export const HOME_ROUTE: Route = { + path: '', + component: HomeComponent, + data: { + authorities: [], + pageTitle: 'home.title' + }, + canActivate: [UserRouteAccessService] +}; diff --git a/jhipster/src/main/webapp/app/home/home.scss b/jhipster/src/main/webapp/app/home/home.scss new file mode 100644 index 0000000000..578787b1c9 --- /dev/null +++ b/jhipster/src/main/webapp/app/home/home.scss @@ -0,0 +1,26 @@ + +/* ========================================================================== +Main page styles +========================================================================== */ + +.hipster { + display: inline-block; + width: 347px; + height: 497px; + background: url("../../content/images/hipster.png") no-repeat center top; + background-size: contain; +} + +/* wait autoprefixer update to allow simple generation of high pixel density media query */ +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and ( min--moz-device-pixel-ratio: 2), +only screen and ( -o-min-device-pixel-ratio: 2/1), +only screen and ( min-device-pixel-ratio: 2), +only screen and ( min-resolution: 192dpi), +only screen and ( min-resolution: 2dppx) { + .hipster { + background: url("../../content/images/hipster2x.png") no-repeat center top; + background-size: contain; + } +} diff --git a/jhipster/src/main/webapp/app/home/index.ts b/jhipster/src/main/webapp/app/home/index.ts new file mode 100644 index 0000000000..d76285b277 --- /dev/null +++ b/jhipster/src/main/webapp/app/home/index.ts @@ -0,0 +1,3 @@ +export * from './home.component'; +export * from './home.route'; +export * from './home.module'; diff --git a/jhipster/src/main/webapp/app/layouts/error/error.component.html b/jhipster/src/main/webapp/app/layouts/error/error.component.html new file mode 100644 index 0000000000..92fe2f1512 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/error/error.component.html @@ -0,0 +1,17 @@ +
+
+
+ +
+
+

Error Page!

+ +
+
{{errorMessage}} +
+
+
You are not authorized to access the page. +
+
+
+
diff --git a/jhipster/src/main/webapp/app/layouts/error/error.component.ts b/jhipster/src/main/webapp/app/layouts/error/error.component.ts new file mode 100644 index 0000000000..983809f541 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/error/error.component.ts @@ -0,0 +1,20 @@ +import { Component, OnInit } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +@Component({ + selector: 'jhi-error', + templateUrl: './error.component.html' +}) +export class ErrorComponent implements OnInit { + errorMessage: string; + error403: boolean; + + constructor( + private jhiLanguageService: JhiLanguageService + ) { + this.jhiLanguageService.setLocations(['error']); + } + + ngOnInit() { + } +} diff --git a/jhipster/src/main/webapp/app/layouts/error/error.route.ts b/jhipster/src/main/webapp/app/layouts/error/error.route.ts new file mode 100644 index 0000000000..5f6085076c --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/error/error.route.ts @@ -0,0 +1,25 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from '../../shared'; +import { ErrorComponent } from './error.component'; + +export const errorRoute: Routes = [ + { + path: 'error', + component: ErrorComponent, + data: { + authorities: [], + pageTitle: 'error.title' + }, + canActivate: [UserRouteAccessService] + }, + { + path: 'accessdenied', + component: ErrorComponent, + data: { + authorities: [], + pageTitle: 'error.title' + }, + canActivate: [UserRouteAccessService] + } +]; diff --git a/jhipster/src/main/webapp/app/layouts/footer/footer.component.html b/jhipster/src/main/webapp/app/layouts/footer/footer.component.html new file mode 100644 index 0000000000..4e4fda05bf --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/footer/footer.component.html @@ -0,0 +1,4 @@ + + diff --git a/jhipster/src/main/webapp/app/layouts/footer/footer.component.ts b/jhipster/src/main/webapp/app/layouts/footer/footer.component.ts new file mode 100644 index 0000000000..37da8bca75 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/footer/footer.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'jhi-footer', + templateUrl: './footer.component.html' +}) +export class FooterComponent {} diff --git a/jhipster/src/main/webapp/app/layouts/index.ts b/jhipster/src/main/webapp/app/layouts/index.ts new file mode 100644 index 0000000000..f25305a0ac --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/index.ts @@ -0,0 +1,10 @@ +export * from './error/error.component'; +export * from './error/error.route'; +export * from './main/main.component'; +export * from './footer/footer.component'; +export * from './navbar/navbar.component'; +export * from './navbar/active-menu.directive'; +export * from './profiles/page-ribbon.component'; +export * from './profiles/profile.service'; +export * from './profiles/profile-info.model'; +export * from './layout-routing.module'; diff --git a/jhipster/src/main/webapp/app/layouts/layout-routing.module.ts b/jhipster/src/main/webapp/app/layouts/layout-routing.module.ts new file mode 100644 index 0000000000..8edbdff26c --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/layout-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes, Resolve } from '@angular/router'; + +import { navbarRoute } from '../app.route'; +import { errorRoute } from './'; + +let LAYOUT_ROUTES = [ + navbarRoute, + ...errorRoute +]; + +@NgModule({ + imports: [ + RouterModule.forRoot(LAYOUT_ROUTES, { useHash: true }) + ], + exports: [ + RouterModule + ] +}) +export class LayoutRoutingModule {} diff --git a/jhipster/src/main/webapp/app/layouts/main/main.component.html b/jhipster/src/main/webapp/app/layouts/main/main.component.html new file mode 100644 index 0000000000..5bcd12ab0b --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/main/main.component.html @@ -0,0 +1,11 @@ + +
+ +
+
+
+ + +
+ +
diff --git a/jhipster/src/main/webapp/app/layouts/main/main.component.ts b/jhipster/src/main/webapp/app/layouts/main/main.component.ts new file mode 100644 index 0000000000..c5982f60ca --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/main/main.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRouteSnapshot, NavigationEnd, RoutesRecognized } from '@angular/router'; + +import { JhiLanguageHelper, StateStorageService } from '../../shared'; + +@Component({ + selector: 'jhi-main', + templateUrl: './main.component.html' +}) +export class JhiMainComponent implements OnInit { + + constructor( + private jhiLanguageHelper: JhiLanguageHelper, + private router: Router, + private $storageService: StateStorageService, + ) {} + + private getPageTitle(routeSnapshot: ActivatedRouteSnapshot) { + let title: string = (routeSnapshot.data && routeSnapshot.data['pageTitle']) ? routeSnapshot.data['pageTitle'] : 'baeldungApp'; + if (routeSnapshot.firstChild) { + title = this.getPageTitle(routeSnapshot.firstChild) || title; + } + return title; + } + + ngOnInit() { + this.router.events.subscribe((event) => { + if (event instanceof NavigationEnd) { + this.jhiLanguageHelper.updateTitle(this.getPageTitle(this.router.routerState.snapshot.root)); + } + if (event instanceof RoutesRecognized) { + let params = {}; + let destinationData = {}; + let destinationName = ''; + let destinationEvent = event.state.root.firstChild.children[0]; + if (destinationEvent !== undefined) { + params = destinationEvent.params; + destinationData = destinationEvent.data; + destinationName = destinationEvent.url[0].path; + } + let from = {name: this.router.url.slice(1)}; + let destination = {name: destinationName, data: destinationData}; + this.$storageService.storeDestinationState(destination, params, from); + } + }); + } +} diff --git a/jhipster/src/main/webapp/app/layouts/navbar/active-menu.directive.ts b/jhipster/src/main/webapp/app/layouts/navbar/active-menu.directive.ts new file mode 100644 index 0000000000..87275c9d57 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/navbar/active-menu.directive.ts @@ -0,0 +1,26 @@ +import { Directive, OnInit, ElementRef, Renderer, Input} from '@angular/core'; +import { TranslateService, LangChangeEvent } from 'ng2-translate'; + +@Directive({ + selector: '[jhiActiveMenu]' +}) +export class ActiveMenuDirective implements OnInit { + @Input() jhiActiveMenu: string; + + constructor(private el: ElementRef, private renderer: Renderer, private translateService: TranslateService) {} + + ngOnInit() { + this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { + this.updateActiveFlag(event.lang); + }); + this.updateActiveFlag(this.translateService.currentLang); + } + + updateActiveFlag(selectedLanguage) { + if (this.jhiActiveMenu === selectedLanguage) { + this.renderer.setElementClass(this.el.nativeElement, 'active', true); + } else { + this.renderer.setElementClass(this.el.nativeElement, 'active', false); + } + } +} diff --git a/jhipster/src/main/webapp/app/layouts/navbar/navbar.component.html b/jhipster/src/main/webapp/app/layouts/navbar/navbar.component.html new file mode 100644 index 0000000000..07b7abb25c --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/navbar/navbar.component.html @@ -0,0 +1,168 @@ + diff --git a/jhipster/src/main/webapp/app/layouts/navbar/navbar.component.ts b/jhipster/src/main/webapp/app/layouts/navbar/navbar.component.ts new file mode 100644 index 0000000000..8f58bfebd9 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/navbar/navbar.component.ts @@ -0,0 +1,81 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { ProfileService } from '../profiles/profile.service'; // FIXME barrel doesnt work here +import { JhiLanguageHelper, Principal, LoginModalService, LoginService } from '../../shared'; + +import { VERSION, DEBUG_INFO_ENABLED } from '../../app.constants'; + +@Component({ + selector: 'jhi-navbar', + templateUrl: './navbar.component.html', + styleUrls: [ + 'navbar.scss' + ] +}) +export class NavbarComponent implements OnInit { + + inProduction: boolean; + isNavbarCollapsed: boolean; + languages: any[]; + swaggerEnabled: boolean; + modalRef: NgbModalRef; + version: string; + + constructor( + private loginService: LoginService, + private languageHelper: JhiLanguageHelper, + private languageService: JhiLanguageService, + private principal: Principal, + private loginModalService: LoginModalService, + private profileService: ProfileService, + private router: Router + ) { + this.version = DEBUG_INFO_ENABLED ? 'v' + VERSION : ''; + this.isNavbarCollapsed = true; + this.languageService.addLocation('home'); + } + + ngOnInit() { + this.languageHelper.getAll().then((languages) => { + this.languages = languages; + }); + + this.profileService.getProfileInfo().subscribe(profileInfo => { + this.inProduction = profileInfo.inProduction; + this.swaggerEnabled = profileInfo.swaggerEnabled; + }); + } + + changeLanguage(languageKey: string) { + this.languageService.changeLanguage(languageKey); + } + + collapseNavbar() { + this.isNavbarCollapsed = true; + } + + isAuthenticated() { + return this.principal.isAuthenticated(); + } + + login() { + this.modalRef = this.loginModalService.open(); + } + + logout() { + this.collapseNavbar(); + this.loginService.logout(); + this.router.navigate(['']); + } + + toggleNavbar() { + this.isNavbarCollapsed = !this.isNavbarCollapsed; + } + + getImageUrl() { + return this.isAuthenticated() ? this.principal.getImageUrl() : null; + } +} diff --git a/jhipster/src/main/webapp/app/layouts/navbar/navbar.scss b/jhipster/src/main/webapp/app/layouts/navbar/navbar.scss new file mode 100644 index 0000000000..d48d609543 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/navbar/navbar.scss @@ -0,0 +1,70 @@ + +/* ========================================================================== +Navbar +========================================================================== */ +.navbar-version { + font-size: 10px; + color: #ccc +} + +.jh-navbar { + background-color: #353d47; + padding: .2em 1em; + .profile-image { + margin: -10px 0px; + height: 40px; + width: 40px; + border-radius: 50%; + } + .dropdown-item.active, .dropdown-item.active:focus, .dropdown-item.active:hover { + background-color: #353d47; + } + .dropdown-toggle::after { + margin-left: 0.15em; + } + ul.navbar-nav { + padding: 0.5em; + .nav-item { + margin-left: 1.5rem; + } + } + a.nav-link { + font-weight: 400; + } + .jh-navbar-toggler { + color: #ccc; + font-size: 1.5em; + padding: 10px; + &:hover { + color: #fff; + } + } +} + +@media screen and (max-width: 992px) { + .jh-logo-container { + width: 100%; + } +} + +.navbar-title { + display: inline-block; + vertical-align: middle; +} + +/* ========================================================================== +Logo styles +========================================================================== */ +.navbar-brand { + &.logo { + padding: 5px 15px; + .logo-img { + height: 45px; + width: 70px; + display: inline-block; + vertical-align: middle; + background: url("../../../content/images/logo-jhipster.png") no-repeat center center; + background-size: contain; + } + } +} diff --git a/jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts b/jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts new file mode 100644 index 0000000000..f7ba492f66 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { ProfileService } from './profile.service'; +import { ProfileInfo } from './profile-info.model'; + +@Component({ + selector: 'jhi-page-ribbon', + template: ``, + styleUrls: [ + 'page-ribbon.scss' + ] +}) +export class PageRibbonComponent implements OnInit { + + profileInfo: ProfileInfo; + ribbonEnv: string; + + constructor(private profileService: ProfileService) {} + + ngOnInit() { + this.profileService.getProfileInfo().subscribe(profileInfo => { + this.profileInfo = profileInfo; + this.ribbonEnv = profileInfo.ribbonEnv; + }); + } +} diff --git a/jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.scss b/jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.scss new file mode 100644 index 0000000000..5efd11c03e --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/profiles/page-ribbon.scss @@ -0,0 +1,32 @@ + +/* ========================================================================== +Developement Ribbon +========================================================================== */ +.ribbon { + background-color: rgba(170, 0, 0, 0.5); + left: -3.5em; + moz-transform: rotate(-45deg); + ms-transform: rotate(-45deg); + o-transform: rotate(-45deg); + webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + overflow: hidden; + position: absolute; + top: 40px; + white-space: nowrap; + width: 15em; + z-index: 9999; + pointer-events: none; + opacity: 0.75; + a { + color: #fff; + display: block; + font-weight: 400; + margin: 1px 0; + padding: 10px 50px; + text-align: center; + text-decoration: none; + text-shadow: 0 0 5px #444; + pointer-events: none; + } +} diff --git a/jhipster/src/main/webapp/app/layouts/profiles/profile-info.model.ts b/jhipster/src/main/webapp/app/layouts/profiles/profile-info.model.ts new file mode 100644 index 0000000000..f1adc52c7b --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/profiles/profile-info.model.ts @@ -0,0 +1,6 @@ +export class ProfileInfo { + activeProfiles: string[]; + ribbonEnv: string; + inProduction: boolean; + swaggerEnabled: boolean; +} diff --git a/jhipster/src/main/webapp/app/layouts/profiles/profile.service.ts b/jhipster/src/main/webapp/app/layouts/profiles/profile.service.ts new file mode 100644 index 0000000000..347b84d8e5 --- /dev/null +++ b/jhipster/src/main/webapp/app/layouts/profiles/profile.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +import { ProfileInfo } from './profile-info.model'; + +@Injectable() +export class ProfileService { + + private profileInfoUrl = 'api/profile-info'; + + constructor(private http: Http) { } + + getProfileInfo(): Observable { + return this.http.get(this.profileInfoUrl) + .map((res: Response) => { + let data = res.json(); + let pi = new ProfileInfo(); + pi.activeProfiles = data.activeProfiles; + pi.ribbonEnv = data.ribbonEnv; + pi.inProduction = data.activeProfiles.indexOf('prod') !== -1; + pi.swaggerEnabled = data.activeProfiles.indexOf('swagger') !== -1; + return pi; + }); + } +} diff --git a/jhipster/src/main/webapp/app/polyfills.ts b/jhipster/src/main/webapp/app/polyfills.ts new file mode 100644 index 0000000000..0771ba0b72 --- /dev/null +++ b/jhipster/src/main/webapp/app/polyfills.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +import 'reflect-metadata/Reflect'; +import 'zone.js/dist/zone'; diff --git a/jhipster/src/main/webapp/app/shared/alert/alert-error.component.ts b/jhipster/src/main/webapp/app/shared/alert/alert-error.component.ts new file mode 100644 index 0000000000..e9e3e2b7a0 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/alert/alert-error.component.ts @@ -0,0 +1,101 @@ +import { Component, OnDestroy } from '@angular/core'; +import { TranslateService } from 'ng2-translate'; +import { EventManager, AlertService } from 'ng-jhipster'; +import { Subscription } from 'rxjs/Rx'; + +@Component({ + selector: 'jhi-alert-error', + template: ` + ` +}) +export class JhiAlertErrorComponent implements OnDestroy { + + alerts: any[]; + cleanHttpErrorListener: Subscription; + + constructor(private alertService: AlertService, private eventManager: EventManager, private translateService: TranslateService) { + this.alerts = []; + + this.cleanHttpErrorListener = eventManager.subscribe('baeldungApp.httpError', (response) => { + let i; + let httpResponse = response.content; + switch (httpResponse.status) { + // connection refused, server not reachable + case 0: + this.addErrorAlert('Server not reachable', 'error.server.not.reachable'); + break; + + case 400: + let arr = Array.from(httpResponse.headers._headers); + let headers = []; + for (i = 0; i < arr.length; i++) { + if (arr[i][0].endsWith('app-error') || arr[i][0].endsWith('app-params')) { + headers.push(arr[i][0]); + } + } + headers.sort(); + let errorHeader = httpResponse.headers.get(headers[0]); + let entityKey = httpResponse.headers.get(headers[1]); + if (errorHeader) { + let entityName = translateService.instant('global.menu.entities.' + entityKey); + this.addErrorAlert(errorHeader, errorHeader, {entityName: entityName}); + } else if (httpResponse.text() !== '' && httpResponse.json() && httpResponse.json().fieldErrors) { + let fieldErrors = httpResponse.json().fieldErrors; + for (i = 0; i < fieldErrors.length; i++) { + let fieldError = fieldErrors[i]; + // convert 'something[14].other[4].id' to 'something[].other[].id' so translations can be written to it + let convertedField = fieldError.field.replace(/\[\d*\]/g, '[]'); + let fieldName = translateService.instant('baeldungApp.' + + fieldError.objectName + '.' + convertedField); + this.addErrorAlert( + 'Field ' + fieldName + ' cannot be empty', 'error.' + fieldError.message, {fieldName: fieldName}); + } + } else if (httpResponse.text() !== '' && httpResponse.json() && httpResponse.json().message) { + this.addErrorAlert(httpResponse.json().message, httpResponse.json().message, httpResponse.json()); + } else { + this.addErrorAlert(httpResponse.text()); + } + break; + + case 404: + this.addErrorAlert('Not found', 'error.url.not.found'); + break; + + default: + if (httpResponse.text() !== '' && httpResponse.json() && httpResponse.json().message) { + this.addErrorAlert(httpResponse.json().message); + } else { + this.addErrorAlert(JSON.stringify(httpResponse)); // Fixme find a way to parse httpResponse + } + } + }); + } + + ngOnDestroy() { + if (this.cleanHttpErrorListener !== undefined && this.cleanHttpErrorListener !== null) { + this.eventManager.destroy(this.cleanHttpErrorListener); + this.alerts = []; + } + } + + addErrorAlert (message, key?, data?) { + key = key && key !== null ? key : message; + this.alerts.push( + this.alertService.addAlert( + { + type: 'danger', + msg: key, + params: data, + timeout: 5000, + toast: this.alertService.isToast(), + scoped: true + }, + this.alerts + ) + ); + } +} diff --git a/jhipster/src/main/webapp/app/shared/alert/alert.component.ts b/jhipster/src/main/webapp/app/shared/alert/alert.component.ts new file mode 100644 index 0000000000..b8aa418ac5 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/alert/alert.component.ts @@ -0,0 +1,26 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { AlertService } from 'ng-jhipster'; + +@Component({ + selector: 'jhi-alert', + template: ` + ` +}) +export class JhiAlertComponent implements OnInit, OnDestroy { + alerts: any[]; + + constructor(private alertService: AlertService) { } + + ngOnInit() { + this.alerts = this.alertService.get(); + } + + ngOnDestroy() { + this.alerts = []; + } + +} diff --git a/jhipster/src/main/webapp/app/shared/auth/account.service.ts b/jhipster/src/main/webapp/app/shared/auth/account.service.ts new file mode 100644 index 0000000000..6d21943d49 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/account.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +@Injectable() +export class AccountService { + constructor(private http: Http) { } + + get(): Observable { + return this.http.get('api/account').map((res: Response) => res.json()); + } + + save(account: any): Observable { + return this.http.post('api/account', account); + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/auth-jwt.service.ts b/jhipster/src/main/webapp/app/shared/auth/auth-jwt.service.ts new file mode 100644 index 0000000000..9be418d175 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/auth-jwt.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, Headers, URLSearchParams } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; +import { LocalStorageService, SessionStorageService } from 'ng2-webstorage'; + +@Injectable() +export class AuthServerProvider { + constructor( + private http: Http, + private $localStorage: LocalStorageService, + private $sessionStorage: SessionStorageService + ) {} + + getToken () { + return this.$localStorage.retrieve('authenticationToken') || this.$sessionStorage.retrieve('authenticationToken'); + } + + login (credentials): Observable { + + let data = { + username: credentials.username, + password: credentials.password, + rememberMe: credentials.rememberMe + }; + return this.http.post('api/authenticate', data).map(authenticateSuccess.bind(this)); + + function authenticateSuccess (resp) { + let bearerToken = resp.headers.get('Authorization'); + if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') { + let jwt = bearerToken.slice(7, bearerToken.length); + this.storeAuthenticationToken(jwt, credentials.rememberMe); + return jwt; + } + } + } + + loginWithToken(jwt, rememberMe) { + if (jwt) { + this.storeAuthenticationToken(jwt, rememberMe); + return Promise.resolve(jwt); + } else { + return Promise.reject('auth-jwt-service Promise reject'); // Put appropriate error message here + } + } + + storeAuthenticationToken(jwt, rememberMe) { + if (rememberMe) { + this.$localStorage.store('authenticationToken', jwt); + } else { + this.$sessionStorage.store('authenticationToken', jwt); + } + } + + logout (): Observable { + return new Observable(observer => { + this.$localStorage.clear('authenticationToken'); + this.$sessionStorage.clear('authenticationToken'); + observer.complete(); + }); + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/auth.service.ts b/jhipster/src/main/webapp/app/shared/auth/auth.service.ts new file mode 100644 index 0000000000..9e21fb6737 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/auth.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +import { LoginModalService } from '../login/login-modal.service'; +import { Principal } from './principal.service'; +import { StateStorageService } from './state-storage.service'; + +@Injectable() +export class AuthService { + + constructor( + private principal: Principal, + private stateStorageService: StateStorageService, + private loginModalService: LoginModalService, + private router: Router + ) {} + + authorize (force) { + let authReturn = this.principal.identity(force).then(authThen.bind(this)); + + return authReturn; + + function authThen () { + let isAuthenticated = this.principal.isAuthenticated(); + let toStateInfo = this.stateStorageService.getDestinationState().destination; + + // an authenticated user can't access to login and register pages + if (isAuthenticated && (toStateInfo.name === 'register' || toStateInfo.name === 'social-auth')) { + this.router.navigate(['']); + return false; + } + + // recover and clear previousState after external login redirect (e.g. oauth2) + let fromStateInfo = this.stateStorageService.getDestinationState().from; + let previousState = this.stateStorageService.getPreviousState(); + if (isAuthenticated && !fromStateInfo.name && previousState) { + this.stateStorageService.resetPreviousState(); + this.router.navigate([previousState.name], { queryParams: previousState.params }); + return false; + } + + if (toStateInfo.data.authorities && toStateInfo.data.authorities.length > 0) { + return this.principal.hasAnyAuthority(toStateInfo.data.authorities).then(hasAnyAuthority => { + if (!hasAnyAuthority) { + if (isAuthenticated) { + // user is signed in but not authorized for desired state + this.router.navigate(['accessdenied']); + } else { + // user is not authenticated. Show the state they wanted before you + // send them to the login service, so you can return them when you're done + let toStateParamsInfo = this.stateStorageService.getDestinationState().params; + this.stateStorageService.storePreviousState(toStateInfo.name, toStateParamsInfo); + // now, send them to the signin state so they can log in + this.router.navigate(['accessdenied']).then(() => { + this.loginModalService.open(); + }); + } + } + return hasAnyAuthority; + }); + } + return true; + } + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/csrf.service.ts b/jhipster/src/main/webapp/app/shared/auth/csrf.service.ts new file mode 100644 index 0000000000..6f1064112a --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/csrf.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { CookieService } from 'angular2-cookie/core'; + +@Injectable() +export class CSRFService { + + constructor(private cookieService: CookieService) {} + + getCSRF(name?: string) { + name = `${name ? name : 'XSRF-TOKEN'}`; + return this.cookieService.get(name); + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/has-any-authority.directive.ts b/jhipster/src/main/webapp/app/shared/auth/has-any-authority.directive.ts new file mode 100644 index 0000000000..858c105fd5 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/has-any-authority.directive.ts @@ -0,0 +1,42 @@ +import { Directive, ElementRef, Input, TemplateRef, ViewContainerRef } from '@angular/core'; +import { Principal } from './principal.service'; + +/** + * @whatItDoes Conditionally includes an HTML element if current user has any + * of the authorities passed as the `expression`. + * + * @howToUse + * ``` + * ... + * + * ... + * ``` + */ +@Directive({ + selector: '[jhiHasAnyAuthority]' +}) +export class HasAnyAuthorityDirective { + + private authorities: string[]; + + constructor(private principal: Principal, private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef) { + } + + @Input() + set jhiHasAnyAuthority(value: string|string[]) { + this.authorities = typeof value === 'string' ? [ value ] : value; + this.updateView(); + // Get notified each time authentication state changes. + this.principal.getAuthenticationState().subscribe(identity => this.updateView()); + } + + private updateView(): void { + this.principal.hasAnyAuthority(this.authorities).then(result => { + if (result) { + this.viewContainerRef.createEmbeddedView(this.templateRef); + } else { + this.viewContainerRef.clear(); + } + }); + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/principal.service.ts b/jhipster/src/main/webapp/app/shared/auth/principal.service.ts new file mode 100644 index 0000000000..2c7f05dd56 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/principal.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { AccountService } from './account.service'; + +@Injectable() +export class Principal { + private userIdentity: any; + private authenticated = false; + private authenticationState = new Subject(); + + constructor( + private account: AccountService + ) {} + + authenticate (identity) { + this.userIdentity = identity; + this.authenticated = identity !== null; + this.authenticationState.next(this.userIdentity); + } + + hasAnyAuthority (authorities: string[]): Promise { + if (!this.authenticated || !this.userIdentity || !this.userIdentity.authorities) { + return Promise.resolve(false); + } + + for (let i = 0; i < authorities.length; i++) { + if (this.userIdentity.authorities.indexOf(authorities[i]) !== -1) { + return Promise.resolve(true); + } + } + + return Promise.resolve(false); + } + + hasAuthority (authority: string): Promise { + if (!this.authenticated) { + return Promise.resolve(false); + } + + return this.identity().then(id => { + return Promise.resolve(id.authorities && id.authorities.indexOf(authority) !== -1); + }, () => { + return Promise.resolve(false); + }); + } + + identity (force?: boolean): Promise { + if (force === true) { + this.userIdentity = undefined; + } + + // check and see if we have retrieved the userIdentity data from the server. + // if we have, reuse it by immediately resolving + if (this.userIdentity) { + return Promise.resolve(this.userIdentity); + } + + // retrieve the userIdentity data from the server, update the identity object, and then resolve. + return this.account.get().toPromise().then(account => { + if (account) { + this.userIdentity = account; + this.authenticated = true; + } else { + this.userIdentity = null; + this.authenticated = false; + } + this.authenticationState.next(this.userIdentity); + return this.userIdentity; + }).catch(err => { + this.userIdentity = null; + this.authenticated = false; + this.authenticationState.next(this.userIdentity); + return null; + }); + } + + isAuthenticated (): boolean { + return this.authenticated; + } + + isIdentityResolved (): boolean { + return this.userIdentity !== undefined; + } + + getAuthenticationState(): Observable { + return this.authenticationState.asObservable(); + } + + getImageUrl(): String { + return this.isIdentityResolved () ? this.userIdentity.imageUrl : null; + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/state-storage.service.ts b/jhipster/src/main/webapp/app/shared/auth/state-storage.service.ts new file mode 100644 index 0000000000..1fe364b06d --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/state-storage.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { SessionStorageService } from 'ng2-webstorage'; + +@Injectable() +export class StateStorageService { + constructor( + private $sessionStorage: SessionStorageService + ) {} + + getPreviousState() { + return this.$sessionStorage.retrieve('previousState'); + } + + resetPreviousState() { + this.$sessionStorage.clear('previousState'); + } + + storePreviousState(previousStateName, previousStateParams) { + let previousState = { 'name': previousStateName, 'params': previousStateParams }; + this.$sessionStorage.store('previousState', previousState); + } + + getDestinationState() { + return this.$sessionStorage.retrieve('destinationState'); + } + + storeDestinationState(destinationState, destinationStateParams, fromState) { + let destinationInfo = { + 'destination': { + 'name': destinationState.name, + 'data': destinationState.data, + }, + 'params': destinationStateParams, + 'from': { + 'name': fromState.name, + } + }; + this.$sessionStorage.store('destinationState', destinationInfo); + } +} diff --git a/jhipster/src/main/webapp/app/shared/auth/user-route-access-service.ts b/jhipster/src/main/webapp/app/shared/auth/user-route-access-service.ts new file mode 100644 index 0000000000..95eb236672 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/auth/user-route-access-service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/router'; + +import { AuthService } from '../'; + +@Injectable() +export class UserRouteAccessService implements CanActivate { + + constructor(private router: Router, private auth: AuthService) { + } + + canActivate(route: ActivatedRouteSnapshot): boolean | Promise { + return this.auth.authorize(false).then( canActivate => canActivate); + } +} diff --git a/jhipster/src/main/webapp/app/shared/constants/pagination.constants.ts b/jhipster/src/main/webapp/app/shared/constants/pagination.constants.ts new file mode 100644 index 0000000000..a148d4579b --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/constants/pagination.constants.ts @@ -0,0 +1 @@ +export const ITEMS_PER_PAGE = 20; diff --git a/jhipster/src/main/webapp/app/shared/index.ts b/jhipster/src/main/webapp/app/shared/index.ts new file mode 100644 index 0000000000..d2687cf884 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/index.ts @@ -0,0 +1,23 @@ +export * from './alert/alert.component'; +export * from './alert/alert-error.component'; +export * from './auth/csrf.service'; +export * from './auth/state-storage.service'; +export * from './auth/account.service'; +export * from './auth/auth-jwt.service'; +export * from './auth/auth.service'; +export * from './auth/principal.service'; +export * from './auth/has-any-authority.directive'; +export * from './language/language.constants'; +export * from './language/language.helper'; +export * from './language/language.pipe'; +export * from './login/login.component'; +export * from './login/login.service'; +export * from './login/login-modal.service'; +export * from './constants/pagination.constants'; +export * from './user/account.model'; +export * from './user/user.model'; +export * from './user/user.service'; +export * from './shared-libs.module'; +export * from './shared-common.module'; +export * from './shared.module'; +export * from './auth/user-route-access-service'; diff --git a/jhipster/src/main/webapp/app/shared/language/language.constants.ts b/jhipster/src/main/webapp/app/shared/language/language.constants.ts new file mode 100644 index 0000000000..2292ef4624 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/language/language.constants.ts @@ -0,0 +1,8 @@ +/* + Languages codes are ISO_639-1 codes, see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + They are written in English to avoid character encoding issues (not a perfect solution) +*/ +export const LANGUAGES: string[] = [ + 'en' + // jhipster-needle-i18n-language-constant - JHipster will add/remove languages in this array +]; diff --git a/jhipster/src/main/webapp/app/shared/language/language.helper.ts b/jhipster/src/main/webapp/app/shared/language/language.helper.ts new file mode 100644 index 0000000000..d8d609bf0c --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/language/language.helper.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Router, ActivatedRouteSnapshot } from '@angular/router'; +import { TranslateService, TranslationChangeEvent, LangChangeEvent } from 'ng2-translate/ng2-translate'; + +import { LANGUAGES } from './language.constants'; + +@Injectable() +export class JhiLanguageHelper { + + constructor (private translateService: TranslateService, private titleService: Title, private router: Router) { + this.init(); + } + + getAll(): Promise { + return Promise.resolve(LANGUAGES); + } + + /** + * Update the window title using params in the following + * order: + * 1. titleKey parameter + * 2. $state.$current.data.pageTitle (current state page title) + * 3. 'global.title' + */ + updateTitle(titleKey?: string) { + if (!titleKey) { + titleKey = this.getPageTitle(this.router.routerState.snapshot.root); + } + + this.translateService.get(titleKey).subscribe(title => { + this.titleService.setTitle(title); + }); + } + + private init () { + this.translateService.onTranslationChange.subscribe((event: TranslationChangeEvent) => { + this.updateTitle(); + }); + + this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { + this.updateTitle(); + }); + } + + private getPageTitle(routeSnapshot: ActivatedRouteSnapshot) { + let title: string = (routeSnapshot.data && routeSnapshot.data['pageTitle']) ? routeSnapshot.data['pageTitle'] : 'baeldungApp'; + if (routeSnapshot.firstChild) { + title = this.getPageTitle(routeSnapshot.firstChild) || title; + } + return title; + } +} diff --git a/jhipster/src/main/webapp/app/shared/language/language.pipe.ts b/jhipster/src/main/webapp/app/shared/language/language.pipe.ts new file mode 100644 index 0000000000..d271c8e2c2 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/language/language.pipe.ts @@ -0,0 +1,42 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({name: 'findLanguageFromKey'}) +export class FindLanguageFromKeyPipe implements PipeTransform { + private languages: any = { + 'ca': 'Català', + 'cs': 'Český', + 'da': 'Dansk', + 'de': 'Deutsch', + 'el': 'Ελληνικά', + 'en': 'English', + 'es': 'Español', + 'et': 'Eesti', + 'fr': 'Français', + 'gl': 'Galego', + 'hu': 'Magyar', + 'hi': 'हिंदी', + 'hy': 'Հայերեն', + 'it': 'Italiano', + 'ja': '日本語', + 'ko': '한국어', + 'mr': 'मराठी', + 'nl': 'Nederlands', + 'pl': 'Polski', + 'pt-br': 'Português (Brasil)', + 'pt-pt': 'Português', + 'ro': 'Română', + 'ru': 'Русский', + 'sk': 'Slovenský', + 'sr': 'Srpski', + 'sv': 'Svenska', + 'ta': 'தமிழ்', + 'th': 'ไทย', + 'tr': 'Türkçe', + 'vi': 'Tiếng Việt', + 'zh-cn': '中文(简体)', + 'zh-tw': '繁體中文' + }; + transform(lang: string): string { + return this.languages[lang]; + } +} diff --git a/jhipster/src/main/webapp/app/shared/login/login-modal.service.ts b/jhipster/src/main/webapp/app/shared/login/login-modal.service.ts new file mode 100644 index 0000000000..9e435978ea --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/login/login-modal.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; + +import { JhiLoginModalComponent } from './login.component'; + +@Injectable() +export class LoginModalService { + private isOpen = false; + constructor ( + private modalService: NgbModal, + ) {} + + open (): NgbModalRef { + if (this.isOpen) { + return; + } + this.isOpen = true; + let modalRef = this.modalService.open(JhiLoginModalComponent); + modalRef.result.then(result => { + this.isOpen = false; + }, (reason) => { + this.isOpen = false; + }); + return modalRef; + } +} diff --git a/jhipster/src/main/webapp/app/shared/login/login.component.html b/jhipster/src/main/webapp/app/shared/login/login.component.html new file mode 100644 index 0000000000..a6b6b19249 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/login/login.component.html @@ -0,0 +1,46 @@ + + diff --git a/jhipster/src/main/webapp/app/shared/login/login.component.ts b/jhipster/src/main/webapp/app/shared/login/login.component.ts new file mode 100644 index 0000000000..90acbb03ac --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/login/login.component.ts @@ -0,0 +1,90 @@ +import { Component, OnInit, AfterViewInit, Renderer, ElementRef } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Router } from '@angular/router'; +import { JhiLanguageService, EventManager } from 'ng-jhipster'; + +import { LoginService } from '../login/login.service'; +import { StateStorageService } from '../auth/state-storage.service'; + +@Component({ + selector: 'jhi-login-modal', + templateUrl: './login.component.html' +}) +export class JhiLoginModalComponent implements OnInit, AfterViewInit { + authenticationError: boolean; + password: string; + rememberMe: boolean; + username: string; + credentials: any; + + constructor( + private eventManager: EventManager, + private languageService: JhiLanguageService, + private loginService: LoginService, + private stateStorageService: StateStorageService, + private elementRef: ElementRef, + private renderer: Renderer, + private router: Router, + public activeModal: NgbActiveModal + ) { + this.credentials = {}; + } + + ngOnInit() { + this.languageService.addLocation('login'); + } + + ngAfterViewInit() { + this.renderer.invokeElementMethod(this.elementRef.nativeElement.querySelector('#username'), 'focus', []); + } + + cancel () { + this.credentials = { + username: null, + password: null, + rememberMe: true + }; + this.authenticationError = false; + this.activeModal.dismiss('cancel'); + } + + login () { + this.loginService.login({ + username: this.username, + password: this.password, + rememberMe: this.rememberMe + }).then(() => { + this.authenticationError = false; + this.activeModal.dismiss('login success'); + if (this.router.url === '/register' || (/activate/.test(this.router.url)) || + this.router.url === '/finishReset' || this.router.url === '/requestReset') { + this.router.navigate(['']); + } + + this.eventManager.broadcast({ + name: 'authenticationSuccess', + content: 'Sending Authentication Success' + }); + + // // previousState was set in the authExpiredInterceptor before being redirected to login modal. + // // since login is succesful, go to stored previousState and clear previousState + let previousState = this.stateStorageService.getPreviousState(); + if (previousState) { + this.stateStorageService.resetPreviousState(); + this.router.navigate([previousState.name], { queryParams: previousState.params }); + } + }).catch(() => { + this.authenticationError = true; + }); + } + + register () { + this.activeModal.dismiss('to state register'); + this.router.navigate(['/register']); + } + + requestResetPassword () { + this.activeModal.dismiss('to state requestReset'); + this.router.navigate(['/reset', 'request']); + } +} diff --git a/jhipster/src/main/webapp/app/shared/login/login.service.ts b/jhipster/src/main/webapp/app/shared/login/login.service.ts new file mode 100644 index 0000000000..2235299225 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/login/login.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { JhiLanguageService } from 'ng-jhipster'; + +import { Principal } from '../auth/principal.service'; +import { AuthServerProvider } from '../auth/auth-jwt.service'; + +@Injectable() +export class LoginService { + + constructor ( + private languageService: JhiLanguageService, + private principal: Principal, + private authServerProvider: AuthServerProvider + ) {} + + login (credentials, callback?) { + let cb = callback || function() {}; + + return new Promise((resolve, reject) => { + this.authServerProvider.login(credentials).subscribe(data => { + this.principal.identity(true).then(account => { + // After the login the language will be changed to + // the language selected by the user during his registration + if (account !== null) { + this.languageService.changeLanguage(account.langKey); + } + resolve(data); + }); + return cb(); + }, err => { + this.logout(); + reject(err); + return cb(err); + }); + }); + } + loginWithToken(jwt, rememberMe) { + return this.authServerProvider.loginWithToken(jwt, rememberMe); + } + + logout () { + this.authServerProvider.logout().subscribe(); + this.principal.authenticate(null); + } +} diff --git a/jhipster/src/main/webapp/app/shared/shared-common.module.ts b/jhipster/src/main/webapp/app/shared/shared-common.module.ts new file mode 100644 index 0000000000..6f713e216b --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/shared-common.module.ts @@ -0,0 +1,47 @@ +import { NgModule, Sanitizer } from '@angular/core'; +import { Title } from '@angular/platform-browser'; + +import { TranslateService } from 'ng2-translate'; +import { AlertService } from 'ng-jhipster'; + +import { + BaeldungSharedLibsModule, + JhiLanguageHelper, + FindLanguageFromKeyPipe, + JhiAlertComponent, + JhiAlertErrorComponent +} from './'; + + +export function alertServiceProvider(sanitizer: Sanitizer, translateService: TranslateService) { + // set below to true to make alerts look like toast + let isToast = false; + return new AlertService(sanitizer, isToast, translateService); +} + +@NgModule({ + imports: [ + BaeldungSharedLibsModule + ], + declarations: [ + FindLanguageFromKeyPipe, + JhiAlertComponent, + JhiAlertErrorComponent + ], + providers: [ + JhiLanguageHelper, + { + provide: AlertService, + useFactory: alertServiceProvider, + deps: [Sanitizer, TranslateService] + }, + Title + ], + exports: [ + BaeldungSharedLibsModule, + FindLanguageFromKeyPipe, + JhiAlertComponent, + JhiAlertErrorComponent + ] +}) +export class BaeldungSharedCommonModule {} diff --git a/jhipster/src/main/webapp/app/shared/shared-libs.module.ts b/jhipster/src/main/webapp/app/shared/shared-libs.module.ts new file mode 100644 index 0000000000..0bf10eeaa8 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/shared-libs.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgJhipsterModule } from 'ng-jhipster'; +import { InfiniteScrollModule } from 'angular2-infinite-scroll'; + +@NgModule({ + imports: [ + NgbModule.forRoot(), + NgJhipsterModule.forRoot({ + i18nEnabled: true, + defaultI18nLang: 'en' + }), + InfiniteScrollModule + ], + exports: [ + FormsModule, + HttpModule, + CommonModule, + NgbModule, + NgJhipsterModule, + InfiniteScrollModule + ] +}) +export class BaeldungSharedLibsModule {} diff --git a/jhipster/src/main/webapp/app/shared/shared.module.ts b/jhipster/src/main/webapp/app/shared/shared.module.ts new file mode 100644 index 0000000000..f7af13852b --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/shared.module.ts @@ -0,0 +1,53 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { DatePipe } from '@angular/common'; + +import { CookieService } from 'angular2-cookie/services/cookies.service'; +import { + BaeldungSharedLibsModule, + BaeldungSharedCommonModule, + CSRFService, + AuthService, + AuthServerProvider, + AccountService, + UserService, + StateStorageService, + LoginService, + LoginModalService, + Principal, + HasAnyAuthorityDirective, + JhiLoginModalComponent +} from './'; + +@NgModule({ + imports: [ + BaeldungSharedLibsModule, + BaeldungSharedCommonModule + ], + declarations: [ + JhiLoginModalComponent, + HasAnyAuthorityDirective + ], + providers: [ + CookieService, + LoginService, + LoginModalService, + AccountService, + StateStorageService, + Principal, + CSRFService, + AuthServerProvider, + AuthService, + UserService, + DatePipe + ], + entryComponents: [JhiLoginModalComponent], + exports: [ + BaeldungSharedCommonModule, + JhiLoginModalComponent, + HasAnyAuthorityDirective, + DatePipe + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + +}) +export class BaeldungSharedModule {} diff --git a/jhipster/src/main/webapp/app/shared/user/account.model.ts b/jhipster/src/main/webapp/app/shared/user/account.model.ts new file mode 100644 index 0000000000..c8b9750760 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/user/account.model.ts @@ -0,0 +1,12 @@ +export class Account { + constructor( + public activated: boolean, + public authorities: string[], + public email: string, + public firstName: string, + public langKey: string, + public lastName: string, + public login: string, + public imageUrl: string + ) { } +} diff --git a/jhipster/src/main/webapp/app/shared/user/user.model.ts b/jhipster/src/main/webapp/app/shared/user/user.model.ts new file mode 100644 index 0000000000..374c3ae0ca --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/user/user.model.ts @@ -0,0 +1,44 @@ +export class User { + public id?: any; + public login?: string; + public firstName?: string; + public lastName?: string; + public email?: string; + public activated?: Boolean; + public langKey?: string; + public authorities?: any[]; + public createdBy?: string; + public createdDate?: Date; + public lastModifiedBy?: string; + public lastModifiedDate?: Date; + public password?: string; + constructor( + id?: any, + login?: string, + firstName?: string, + lastName?: string, + email?: string, + activated?: Boolean, + langKey?: string, + authorities?: any[], + createdBy?: string, + createdDate?: Date, + lastModifiedBy?: string, + lastModifiedDate?: Date, + password?: string + ) { + this.id = id ? id : null; + this.login = login ? login : null; + this.firstName = firstName ? firstName : null; + this.lastName = lastName ? lastName : null; + this.email = email ? email : null; + this.activated = activated ? activated : false; + this.langKey = langKey ? langKey : null; + this.authorities = authorities ? authorities : null; + this.createdBy = createdBy ? createdBy : null; + this.createdDate = createdDate ? createdDate : null; + this.lastModifiedBy = lastModifiedBy ? lastModifiedBy : null; + this.lastModifiedDate = lastModifiedDate ? lastModifiedDate : null; + this.password = password ? password : null; + } +} diff --git a/jhipster/src/main/webapp/app/shared/user/user.service.ts b/jhipster/src/main/webapp/app/shared/user/user.service.ts new file mode 100644 index 0000000000..b0e1121215 --- /dev/null +++ b/jhipster/src/main/webapp/app/shared/user/user.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, URLSearchParams } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; + +import { User } from './user.model'; + +@Injectable() +export class UserService { + private resourceUrl = 'api/users'; + + constructor(private http: Http) { } + + create(user: User): Observable { + return this.http.post(this.resourceUrl, user); + } + + update(user: User): Observable { + return this.http.put(this.resourceUrl, user); + } + + find(login: string): Observable { + return this.http.get(`${this.resourceUrl}/${login}`).map((res: Response) => res.json()); + } + + query(req?: any): Observable { + let params: URLSearchParams = new URLSearchParams(); + if (req) { + params.set('page', req.page); + params.set('size', req.size); + if (req.sort) { + params.paramsMap.set('sort', req.sort); + } + } + + let options = { + search: params + }; + + return this.http.get(this.resourceUrl, options); + } + + delete(login: string): Observable { + return this.http.delete(`${this.resourceUrl}/${login}`); + } +} diff --git a/jhipster/src/main/webapp/app/vendor.ts b/jhipster/src/main/webapp/app/vendor.ts new file mode 100644 index 0000000000..f9ccfd5b00 --- /dev/null +++ b/jhipster/src/main/webapp/app/vendor.ts @@ -0,0 +1,3 @@ +/* after changing this file run 'npm install' or 'npm run webpack:build' */ +/* tslint:disable */ +import '../content/scss/vendor.scss'; diff --git a/jhipster/src/main/webapp/content/images/hipster.png b/jhipster/src/main/webapp/content/images/hipster.png new file mode 100644 index 0000000000000000000000000000000000000000..141778d482528a8c6636d56d6683f74b21a34394 GIT binary patch literal 9499 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 literal 0 HcmV?d00001 diff --git a/jhipster/src/main/webapp/content/images/logo-jhipster.png b/jhipster/src/main/webapp/content/images/logo-jhipster.png new file mode 100644 index 0000000000000000000000000000000000000000..d8eb48da05c157571eed9fd7303f8d9566f2ea12 GIT binary patch literal 4459 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 { + dd { + margin-bottom: 15px; + } +} + +@media screen and (min-width: 768px) { + .row.jh-entity-details > { + dt { + margin-bottom: 15px; + } + dd { + border-bottom: 1px solid #eee; + padding-left: 180px; + margin-left: 0; + } + } +} + +/* ========================================================================== +ui bootstrap tweaks +========================================================================== */ +.nav, .pagination, .carousel, .panel-title a { + cursor: pointer; +} + +.datetime-picker-dropdown > li.date-picker-menu div > table .btn-default, +.uib-datepicker-popup > li > div.uib-datepicker > table .btn-default { + border: 0; +} + +.datetime-picker-dropdown > li.date-picker-menu div > table:focus, +.uib-datepicker-popup > li > div.uib-datepicker > table:focus { + outline: none; +} + + +/* jhipster-needle-scss-add-main JHipster will add new css style */ diff --git a/jhipster/src/main/webapp/content/scss/vendor.scss b/jhipster/src/main/webapp/content/scss/vendor.scss new file mode 100644 index 0000000000..55990bbef5 --- /dev/null +++ b/jhipster/src/main/webapp/content/scss/vendor.scss @@ -0,0 +1,10 @@ +/* after changing this file run 'npm install' or 'npm run webpack:build' */ +$fa-font-path: '~font-awesome/fonts'; + +/*************************** +put Sass variables here: +eg $input-color: red; +****************************/ + +@import 'node_modules/bootstrap/scss/bootstrap'; +@import 'node_modules/font-awesome/scss/font-awesome'; diff --git a/jhipster/src/main/webapp/favicon.ico b/jhipster/src/main/webapp/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..44c7595f58f5a2c52f5e21451e5e6868d6caea7f GIT binary patch literal 5430 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 user has been activated. Please ", + "error": "Your user could not be activated. Please use the registration form to sign up." + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/audits.json b/jhipster/src/main/webapp/i18n/en/audits.json new file mode 100644 index 0000000000..ed5e16d4c5 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/audits.json @@ -0,0 +1,27 @@ +{ + "audits": { + "title": "Audits", + "filter": { + "title": "Filter per date", + "from": "from", + "to": "to", + "button": { + "weeks": "Weeks", + "today": "today", + "clear": "clear", + "close": "close" + } + }, + "table": { + "header": { + "principal": "User", + "date": "Date", + "status": "State", + "data": "Extra data" + }, + "data": { + "remoteAddress": "Remote Address:" + } + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/comment.json b/jhipster/src/main/webapp/i18n/en/comment.json new file mode 100644 index 0000000000..eed148855a --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/comment.json @@ -0,0 +1,23 @@ +{ + "baeldungApp": { + "comment" : { + "home": { + "title": "Comments", + "createLabel": "Create a new Comment", + "createOrEditLabel": "Create or edit a Comment" + }, + "created": "A new Comment is created with identifier {{ param }}", + "updated": "A Comment is updated with identifier {{ param }}", + "deleted": "A Comment is deleted with identifier {{ param }}", + "delete": { + "question": "Are you sure you want to delete Comment {{ id }}?" + }, + "detail": { + "title": "Comment" + }, + "text": "Text", + "creationDate": "Creation Date", + "post": "Post" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/configuration.json b/jhipster/src/main/webapp/i18n/en/configuration.json new file mode 100644 index 0000000000..81e208de5c --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/configuration.json @@ -0,0 +1,10 @@ +{ + "configuration": { + "title": "Configuration", + "filter": "Filter (by prefix)", + "table": { + "prefix": "Prefix", + "properties": "Properties" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/error.json b/jhipster/src/main/webapp/i18n/en/error.json new file mode 100644 index 0000000000..65246c3b03 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/error.json @@ -0,0 +1,6 @@ +{ + "error": { + "title": "Error page!", + "403": "You are not authorized to access the page." + } +} diff --git a/jhipster/src/main/webapp/i18n/en/gateway.json b/jhipster/src/main/webapp/i18n/en/gateway.json new file mode 100644 index 0000000000..8c8bbd7e98 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/gateway.json @@ -0,0 +1,15 @@ +{ + "gateway": { + "title": "Gateway", + "routes": { + "title": "Current routes", + "url": "URL", + "service": "service", + "servers": "Available servers", + "error": "Warning: no server available!" + }, + "refresh": { + "button": "Refresh" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/global.json b/jhipster/src/main/webapp/i18n/en/global.json new file mode 100644 index 0000000000..0c49362336 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/global.json @@ -0,0 +1,133 @@ +{ + "global": { + "title": "Baeldung", + "browsehappy": "You are using an outdated browser. Please upgrade your browser to improve your experience.", + "menu": { + "home": "Home", + "jhipster-needle-menu-add-element": "JHipster will add additional menu entries here (do not translate!)", + "entities": { + "main": "Entities", + "post": "Post", + "comment": "Comment", + "jhipster-needle-menu-add-entry": "JHipster will add additional entities here (do not translate!)" + }, + "account": { + "main": "Account", + "settings": "Settings", + "password": "Password", + "sessions": "Sessions", + "login": "Sign in", + "logout": "Sign out", + "register": "Register" + }, + "admin": { + "main": "Administration", + "userManagement": "User management", + "tracker": "User tracker", + "metrics": "Metrics", + "health": "Health", + "configuration": "Configuration", + "logs": "Logs", + "audits": "Audits", + "apidocs": "API", + "database": "Database", + "jhipster-needle-menu-add-admin-element": "JHipster will add additional menu entries here (do not translate!)" + }, + "language": "Language" + }, + "form": { + "username": "Username", + "username.placeholder": "Your username", + "newpassword": "New password", + "newpassword.placeholder": "New password", + "confirmpassword": "New password confirmation", + "confirmpassword.placeholder": "Confirm the new password", + "email": "E-mail", + "email.placeholder": "Your e-mail" + }, + "messages": { + "info": { + "authenticated": { + "prefix": "If you want to ", + "link": "sign in", + "suffix": ", you can try the default accounts:
- Administrator (login=\"admin\" and password=\"admin\")
- User (login=\"user\" and password=\"user\")." + }, + "register": { + "noaccount": "You don't have an account yet?", + "link": "Register a new account" + } + }, + "error": { + "dontmatch": "The password and its confirmation do not match!" + }, + "validate": { + "newpassword": { + "required": "Your password is required.", + "minlength": "Your password is required to be at least 4 characters.", + "maxlength": "Your password cannot be longer than 50 characters.", + "strength": "Password strength:" + }, + "confirmpassword": { + "required": "Your confirmation password is required.", + "minlength": "Your confirmation password is required to be at least 4 characters.", + "maxlength": "Your confirmation password cannot be longer than 50 characters." + }, + "email": { + "required": "Your e-mail is required.", + "invalid": "Your e-mail is invalid.", + "minlength": "Your e-mail is required to be at least 5 characters.", + "maxlength": "Your e-mail cannot be longer than 50 characters." + } + } + }, + "field": { + "id": "ID" + }, + "ribbon": { + "dev":"Development" + } + }, + "entity": { + "action": { + "addblob": "Add blob", + "addimage": "Add image", + "back": "Back", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "open": "Open", + "save": "Save", + "view": "View" + }, + "detail": { + "field": "Field", + "value": "Value" + }, + "delete": { + "title": "Confirm delete operation" + }, + "validation": { + "required": "This field is required.", + "minlength": "This field is required to be at least {{ min }} characters.", + "maxlength": "This field cannot be longer than {{ max }} characters.", + "min": "This field should be at least {{ min }}.", + "max": "This field cannot be more than {{ max }}.", + "minbytes": "This field should be at least {{ min }} bytes.", + "maxbytes": "This field cannot be more than {{ max }} bytes.", + "pattern": "This field should follow pattern {{ pattern }}.", + "number": "This field should be a number.", + "datetimelocal": "This field should be a date and time." + } + }, + "error": { + "internalServerError": "Internal server error", + "server.not.reachable": "Server not reachable", + "url.not.found": "Not found", + "NotNull": "Field {{ fieldName }} cannot be empty!", + "Size": "Field {{ fieldName }} does not meet min/max size requirements!", + "userexists": "Login name already used!", + "emailexists": "E-mail is already in use!", + "idexists": "A new {{ entityName }} cannot already have an ID" + }, + "footer": "This is your footer" +} diff --git a/jhipster/src/main/webapp/i18n/en/health.json b/jhipster/src/main/webapp/i18n/en/health.json new file mode 100644 index 0000000000..868ea3fda4 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/health.json @@ -0,0 +1,27 @@ +{ + "health": { + "title": "Health Checks", + "refresh.button": "Refresh", + "stacktrace": "Stacktrace", + "details": { + "details": "Details", + "properties": "Properties", + "name": "Name", + "value": "Value", + "error": "Error" + }, + "indicator": { + "diskSpace": "Disk space", + "mail": "Email", + "db": "Database" + }, + "table": { + "service": "Service name", + "status": "Status" + }, + "status": { + "UP": "UP", + "DOWN": "DOWN" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/home.json b/jhipster/src/main/webapp/i18n/en/home.json new file mode 100644 index 0000000000..402f18700a --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/home.json @@ -0,0 +1,19 @@ +{ + "home": { + "title": "Welcome, Java Hipster!", + "subtitle": "This is your homepage", + "logged": { + "message": "You are logged in as user \"{{username}}\"." + }, + "question": "If you have any question on JHipster:", + "link": { + "homepage": "JHipster homepage", + "stackoverflow": "JHipster on Stack Overflow", + "bugtracker": "JHipster bug tracker", + "chat": "JHipster public chat room", + "follow": "follow @java_hipster on Twitter" + }, + "like": "If you like JHipster, don't forget to give us a star on", + "github": "GitHub" + } +} diff --git a/jhipster/src/main/webapp/i18n/en/login.json b/jhipster/src/main/webapp/i18n/en/login.json new file mode 100644 index 0000000000..3a000852e6 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/login.json @@ -0,0 +1,19 @@ +{ + "login": { + "title": "Sign in", + "form": { + "password": "Password", + "password.placeholder": "Your password", + "rememberme": "Remember me", + "button": "Sign in" + }, + "messages": { + "error": { + "authentication": "Failed to sign in! Please check your credentials and try again." + } + }, + "password" : { + "forgot": "Did you forget your password?" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/logs.json b/jhipster/src/main/webapp/i18n/en/logs.json new file mode 100644 index 0000000000..a614b128da --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/logs.json @@ -0,0 +1,11 @@ +{ + "logs": { + "title": "Logs", + "nbloggers": "There are {{ total }} loggers.", + "filter": "Filter", + "table": { + "name": "Name", + "level": "Level" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/metrics.json b/jhipster/src/main/webapp/i18n/en/metrics.json new file mode 100644 index 0000000000..9d804947c5 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/metrics.json @@ -0,0 +1,101 @@ +{ + "metrics": { + "title": "Application Metrics", + "refresh.button": "Refresh", + "updating": "Updating...", + "jvm": { + "title": "JVM Metrics", + "memory": { + "title": "Memory", + "total": "Total Memory", + "heap": "Heap Memory", + "nonheap": "Non-Heap Memory" + }, + "threads": { + "title": "Threads", + "all": "All", + "runnable": "Runnable", + "timedwaiting": "Timed waiting", + "waiting": "Waiting", + "blocked": "Blocked", + "dump": { + "title": "Threads dump", + "id": "Id: ", + "blockedtime": "Blocked Time", + "blockedcount": "Blocked Count", + "waitedtime": "Waited Time", + "waitedcount": "Waited Count", + "lockname": "Lock name", + "stacktrace": "Stacktrace", + "show": "Show Stacktrace", + "hide": "Hide Stacktrace" + } + }, + "gc": { + "title": "Garbage collections", + "marksweepcount": "Mark Sweep count", + "marksweeptime": "Mark Sweep time", + "scavengecount": "Scavenge count", + "scavengetime": "Scavenge time" + }, + "http": { + "title": "HTTP requests (events per second)", + "active": "Active requests:", + "total": "Total requests:", + "table": { + "code": "Code", + "count": "Count", + "mean": "Mean", + "average": "Average" + }, + "code": { + "ok": "Ok", + "notfound": "Not found", + "servererror": "Server Error" + } + } + }, + "servicesstats": { + "title": "Services statistics (time in millisecond)", + "table": { + "name": "Service name", + "count": "Count", + "mean": "Mean", + "min": "Min", + "max": "Max", + "p50": "p50", + "p75": "p75", + "p95": "p95", + "p99": "p99" + } + }, + "cache": { + "title": "Cache statistics", + "cachename": "Cache name", + "hits": "Cache Hits", + "misses": "Cache Misses", + "gets": "Cache Gets", + "puts": "Cache Puts", + "removals": "Cache Removals", + "evictions": "Cache Evictions", + "hitPercent": "Cache Hit %", + "missPercent": "Cache Miss %", + "averageGetTime": "Average get time (µs)", + "averagePutTime": "Average put time (µs)", + "averageRemoveTime": "Average remove time (µs)" + }, + "datasource": { + "usage": "Usage", + "title": "DataSource statistics (time in millisecond)", + "name": "Pool usage", + "count": "Count", + "mean": "Mean", + "min": "Min", + "max": "Max", + "p50": "p50", + "p75": "p75", + "p95": "p95", + "p99": "p99" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/password.json b/jhipster/src/main/webapp/i18n/en/password.json new file mode 100644 index 0000000000..46227a7702 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/password.json @@ -0,0 +1,12 @@ +{ + "password": { + "title": "Password for [{{username}}]", + "form": { + "button": "Save" + }, + "messages": { + "error": "An error has occurred! The password could not be changed.", + "success": "Password changed!" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/post.json b/jhipster/src/main/webapp/i18n/en/post.json new file mode 100644 index 0000000000..14c64f3f90 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/post.json @@ -0,0 +1,24 @@ +{ + "baeldungApp": { + "post" : { + "home": { + "title": "Posts", + "createLabel": "Create a new Post", + "createOrEditLabel": "Create or edit a Post" + }, + "created": "A new Post is created with identifier {{ param }}", + "updated": "A Post is updated with identifier {{ param }}", + "deleted": "A Post is deleted with identifier {{ param }}", + "delete": { + "question": "Are you sure you want to delete Post {{ id }}?" + }, + "detail": { + "title": "Post" + }, + "title": "Title", + "content": "Content", + "creationDate": "Creation Date", + "creator": "Creator" + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/register.json b/jhipster/src/main/webapp/i18n/en/register.json new file mode 100644 index 0000000000..df8f6e31b8 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/register.json @@ -0,0 +1,24 @@ +{ + "register": { + "title": "Registration", + "form": { + "button": "Register" + }, + "messages": { + "validate": { + "login": { + "required": "Your username is required.", + "minlength": "Your username is required to be at least 1 character.", + "maxlength": "Your username cannot be longer than 50 characters.", + "pattern": "Your username can only contain lower-case letters and digits." + } + }, + "success": "Registration saved! Please check your email for confirmation.", + "error": { + "fail": "Registration failed! Please try again later.", + "userexists": "Login name already registered! Please choose another one.", + "emailexists": "E-mail is already in use! Please choose another one." + } + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/reset.json b/jhipster/src/main/webapp/i18n/en/reset.json new file mode 100644 index 0000000000..fc61e0070f --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/reset.json @@ -0,0 +1,27 @@ +{ + "reset": { + "request": { + "title": "Reset your password", + "form": { + "button": "Reset password" + }, + "messages": { + "info": "Enter the e-mail address you used to register", + "success": "Check your e-mails for details on how to reset your password.", + "notfound": "E-Mail address isn't registered! Please check and try again" + } + }, + "finish" : { + "title": "Reset password", + "form": { + "button": "Validate new password" + }, + "messages": { + "info": "Choose a new password", + "success": "Your password has been reset. Please ", + "keymissing": "The reset key is missing.", + "error": "Your password couldn't be reset. Remember a password request is only valid for 24 hours." + } + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/sessions.json b/jhipster/src/main/webapp/i18n/en/sessions.json new file mode 100644 index 0000000000..d410035ee7 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/sessions.json @@ -0,0 +1,15 @@ +{ + "sessions": { + "title": "Active sessions for [{{username}}]", + "table": { + "ipaddress": "IP address", + "useragent": "User Agent", + "date": "Date", + "button": "Invalidate" + }, + "messages": { + "success": "Session invalidated!", + "error": "An error has occurred! The session could not be invalidated." + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/settings.json b/jhipster/src/main/webapp/i18n/en/settings.json new file mode 100644 index 0000000000..919ab51cc9 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/settings.json @@ -0,0 +1,32 @@ +{ + "settings": { + "title": "User settings for [{{username}}]", + "form": { + "firstname": "First Name", + "firstname.placeholder": "Your first name", + "lastname": "Last Name", + "lastname.placeholder": "Your last name", + "language": "Language", + "button": "Save" + }, + "messages": { + "error": { + "fail": "An error has occurred! Settings could not be saved.", + "emailexists": "E-mail is already in use! Please choose another one." + }, + "success": "Settings saved!", + "validate": { + "firstname": { + "required": "Your first name is required.", + "minlength": "Your first name is required to be at least 1 character", + "maxlength": "Your first name cannot be longer than 50 characters" + }, + "lastname": { + "required": "Your last name is required.", + "minlength": "Your last name is required to be at least 1 character", + "maxlength": "Your last name cannot be longer than 50 characters" + } + } + } + } +} diff --git a/jhipster/src/main/webapp/i18n/en/user-management.json b/jhipster/src/main/webapp/i18n/en/user-management.json new file mode 100644 index 0000000000..30c125b6d0 --- /dev/null +++ b/jhipster/src/main/webapp/i18n/en/user-management.json @@ -0,0 +1,30 @@ +{ + "userManagement": { + "home": { + "title": "Users", + "createLabel": "Create a new user", + "createOrEditLabel": "Create or edit a user" + }, + "created": "A new user is created with identifier {{ param }}", + "updated": "An user is updated with identifier {{ param }}", + "deleted": "An user is deleted with identifier {{ param }}", + "delete": { + "question": "Are you sure you want to delete user {{ login }}?" + }, + "detail": { + "title": "User" + }, + "login": "Login", + "firstName": "First name", + "lastName": "Last name", + "email": "Email", + "activated": "Activated", + "deactivated": "Deactivated", + "profiles": "Profiles", + "langKey": "Language", + "createdBy": "Created by", + "createdDate": "Created date", + "lastModifiedBy": "Modified by", + "lastModifiedDate": "Modified date" + } +} diff --git a/jhipster/src/main/webapp/index.html b/jhipster/src/main/webapp/index.html new file mode 100644 index 0000000000..6227d62823 --- /dev/null +++ b/jhipster/src/main/webapp/index.html @@ -0,0 +1,27 @@ + + + + + + + baeldung + + + + + + + + + + diff --git a/jhipster/src/main/webapp/robots.txt b/jhipster/src/main/webapp/robots.txt new file mode 100644 index 0000000000..7de2585cf6 --- /dev/null +++ b/jhipster/src/main/webapp/robots.txt @@ -0,0 +1,11 @@ +# robotstxt.org/ + +User-agent: * +Disallow: /api/account +Disallow: /api/account/change_password +Disallow: /api/account/sessions +Disallow: /api/audits/ +Disallow: /api/logs/ +Disallow: /api/users/ +Disallow: /management/ +Disallow: /v2/api-docs/ diff --git a/jhipster/src/main/webapp/swagger-ui/images/throbber.gif b/jhipster/src/main/webapp/swagger-ui/images/throbber.gif new file mode 100644 index 0000000000000000000000000000000000000000..06393889242fb3ea9e0205fa84369ec7bb66d15a GIT binary patch literal 9257 zcmd^^X;@R|x`tQg5wbE8AV3mAn1TjmQ&en2CK8~ENEH<+P_)pZ24y2E+7O0>K^a6u zQ3;5MiU^7p6*M3qDk!2=YEcHMQ>nzEYP;R`e2C@r+U+?#XaC*&gKPcB#k$`o&;7mu zYNhYYXe|Uo84#4ZIko#rcU5K8*yFL{qT47O&^5fZH$ zVZ@%(l~vVHjnm;H@KL8@r%yUHoo;rbHI_4lIH(_nsTT>S2`DFOD~uCb9_dF4`#QgI zy7ldMcLs+A_s%|e1pRPrbX-tpeNP!9(IpMFTce`t_5U%lP99z%&i6`1d~ zWeM!Rxc50<+d$e^9LT`?B+aMK~apR zHm?q;p<7{wN2g|I^aGlSws;VP84j(z%aQwvAWv83Z$}p(% zZ^?2;gxg(ey_`V5J7{;!o;o;KslW@z5EP~JGs|U)J7dF&(ff#A=6vU?cGQ$-4+;Jf z-ggJEa!yStn`_EWvl)#yhm6XVs}UUbsi;+agri;mCfjH^Uy;lH+Zw^h)4N?oZgZz4 zJk(fTZ|Bi^;+s_M=~+d#vyoxEPzTlOS=mX@sbl*uRj>=MaMr}cFIY8i?UM61>86uB zV$DlOUCiUJwbzJMP@D$urzK|lL2-PC!p1l47V-ZG<5Ev0Z5h~Kx?`KOp7gkAjV93A z-Gc7MrlxTf?wF;CbNc@tCHJH{TB3c;#{SVu%97}tyAM2n&|9W_?qv}$*Jt*%7Yxb# zV0;d;7|lDEltJYS+U)#aiJO};?_Jyy_4%syQ(uy?-J-Yx-9O5nKRk@@XSS~X<(2u~ zV-LamWm~!iqtH9wkpf8mAXZhOD&L#aA_%)4h2M;1M5jt zIR>Us+%W-GXa_f^opKg=DSrAs)AXeRa;Hp0aC1OgbxQ%Qr_QvTleM1jkR!2mkcX$3 ztsR8~G9iqh(-FJ@F_rQBIYDXV_6s7G9SxaVF^laZqcx$!D97m|7t16j6@Jt6UdDRy49Qyvs|c>RuA|@b%}`*wU}2^7q;&Vtc6@lb zcXl)T!6nYDzmMJ~%n$KNXyNlCG)GkJ4!82;v6@d3>s5r~E+3!O?049JDr14Y^PeMI02R`0lJ^=oJ zYd|*u9|SU(j7hY?+<=(?fP*mtV*zFhOrz6%{VA?ozdm&(Jf^V zMfPZ?>l`mS3{Uq8IM;e!+1YjJy2!mzK$O|wPeU{*QSbs9m+@`f5KxO3PBnQ=%RsZg%go*fJ`*w9TL{-WgZVIA$!YV}3BRcfeXaR$x#b zW)Tpd#8E4)^MyYdkH;4_;ChJuw%n+Be7Ko4;w-nHvyo$d_0e-YiF78Df&)_)(}fcr_r0mPH(4RRYWIu+d@t0&Ss@O^s! zOKyX&13)%N@83r^;QsgN{rl(!0|RF1FA)b1{CRXAy&1ySz@>olPiR4r$aMdq&_=nK zq|cFs8phWJ1@%dZ-gXd{zDbTILD>)qEvH-NU*Rf1b2J1Ri79`rBFl@ z8E^0I)OqEi{pH(a24b9YPG;Kz@t-qZW;3Mpe`MRlmYx{7bH-XZ&`RQ7Rb^%}gc&X| zd}Q-FZf|RWxHU?PR!(C?80zu(^l>*h{#ulSiid(O!J(8P-41bNM3tnX@U6NS5yo0? zdcF)~xFE&+&|gZ$23dV5t~?$$&ymZ;F8j7GGMncGSsDo%>J`26=&l=X#rSKv_64;0 zr;k6no@=gV`P)K!=kaHl>q?!`X>(A;84tg^Md<`zA%qbRLby1Z=fn*ZRdNqs%Tq|3 zOt}lZu0q9oKJhgz&+^7PCt$=UFW=R*w?a1)ePoL*`R$Gxj?TU@12tTHsT$giHQU+sqf;fS0FpT!< z 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 + + + + Swagger UI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+
+ + diff --git a/jhipster/src/test/gatling/conf/gatling.conf b/jhipster/src/test/gatling/conf/gatling.conf new file mode 100644 index 0000000000..e509853f22 --- /dev/null +++ b/jhipster/src/test/gatling/conf/gatling.conf @@ -0,0 +1,131 @@ +######################### +# Gatling Configuration # +######################### + +# This file contains all the settings configurable for Gatling with their default values + +gatling { + core { + #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) + #runDescription = "" # The description for this simulation run, displayed in each report + #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation + #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) + #mute = false # When set to true, don't ask for simulation name nor run description (currently only used by Gatling SBT plugin) + #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable + #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body Raw templates, set to 0 to disable + #rawFileBodiesInMemoryMaxSize = 1000 # Below this limit, raw file bodies will be cached in memory + + extract { + regex { + #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching + } + xpath { + #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching + } + jsonPath { + #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching + #preferJackson = false # When set to true, prefer Jackson over Boon for JSON-related operations + } + css { + #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching + } + } + directory { + #data = user-files/data # Folder where user's data (e.g. files used by Feeders) is located + #bodies = user-files/bodies # Folder where bodies are located + #simulations = user-files/simulations # Folder where the bundle's simulations are located + #reportsOnly = "" # If set, name of report folder to look for in order to generate its report + #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. + #results = results # Name of the folder where all reports folder are located + } + } + charting { + #noReports = false # When set to true, don't generate HTML reports + #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports + #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. + indicators { + #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary + #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary + #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite + #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite + #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite + #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite + } + } + http { + #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable + #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable + #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable + #warmUpUrl = "http://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) + #enableGA = true # Very light Google Analytics, please support + ssl { + keyStore { + #type = "" # Type of SSLContext's KeyManagers store + #file = "" # Location of SSLContext's KeyManagers store + #password = "" # Password for SSLContext's KeyManagers store + #algorithm = "" # Algorithm used SSLContext's KeyManagers store + } + trustStore { + #type = "" # Type of SSLContext's TrustManagers store + #file = "" # Location of SSLContext's TrustManagers store + #password = "" # Password for SSLContext's TrustManagers store + #algorithm = "" # Algorithm used by SSLContext's TrustManagers store + } + } + ahc { + #keepAlive = true # Allow pooling HTTP connections (keep-alive header automatically added) + #connectTimeout = 10000 # Timeout when establishing a connection + #handshakeTimeout = 10000 # Timeout when performing TLS hashshake + #pooledConnectionIdleTimeout = 60000 # Timeout when a connection stays unused in the pool + #readTimeout = 60000 # Timeout when a used connection stays idle + #maxRetry = 2 # Number of times that a request should be tried again + #requestTimeout = 60000 # Timeout of the requests + #acceptAnyCertificate = true # When set to true, doesn't validate SSL certificates + #httpClientCodecMaxInitialLineLength = 4096 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") + #httpClientCodecMaxHeaderSize = 8192 # Maximum size, in bytes, of each request's headers + #httpClientCodecMaxChunkSize = 8192 # Maximum length of the content or each chunk + #webSocketMaxFrameSize = 10240000 # Maximum frame payload size + #sslEnabledProtocols = [TLSv1.2, TLSv1.1, TLSv1] # Array of enabled protocols for HTTPS, if empty use the JDK defaults + #sslEnabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty use the AHC defaults + #sslSessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default + #sslSessionTimeout = 0 # SSLSession timeout in seconds, set to 0 to use JDK's default (24h) + #useOpenSsl = false # if OpenSSL should be used instead of JSSE (requires tcnative jar) + #useNativeTransport = false # if native transport should be used instead of Java NIO (requires netty-transport-native-epoll, currently Linux only) + #tcpNoDelay = true + #soReuseAddress = false + #soLinger = -1 + #soSndBuf = -1 + #soRcvBuf = -1 + #allocator = "pooled" # switch to unpooled for unpooled ByteBufAllocator + #maxThreadLocalCharBufferSize = 200000 # Netty's default is 16k + } + dns { + #queryTimeout = 5000 # Timeout of each DNS query in millis + #maxQueriesPerResolve = 6 # Maximum allowed number of DNS queries for a given name resolution + } + } + jms { + #acknowledgedMessagesBufferSize = 5000 # size of the buffer used to tracked acknowledged messages and protect against duplicate receives + } + data { + #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite, jdbc) + console { + #light = false # When set to true, displays a light version without detailed request stats + } + file { + #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes + } + leak { + #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening + } + graphite { + #light = false # only send the all* stats + #host = "localhost" # The host where the Carbon server is located + #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) + #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") + #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite + #bufferSize = 8192 # GraphiteDataWriter's internal data buffer size, in bytes + #writeInterval = 1 # GraphiteDataWriter's write interval, in seconds + } + } +} diff --git a/jhipster/src/test/gatling/conf/logback.xml b/jhipster/src/test/gatling/conf/logback.xml new file mode 100644 index 0000000000..7b037e6813 --- /dev/null +++ b/jhipster/src/test/gatling/conf/logback.xml @@ -0,0 +1,22 @@ + + + + + + %d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx + false + + + + + + + + + + + + + + + diff --git a/jhipster/src/test/gatling/simulations/CommentGatlingTest.scala b/jhipster/src/test/gatling/simulations/CommentGatlingTest.scala new file mode 100644 index 0000000000..93d066f9bb --- /dev/null +++ b/jhipster/src/test/gatling/simulations/CommentGatlingTest.scala @@ -0,0 +1,92 @@ +import _root_.io.gatling.core.scenario.Simulation +import ch.qos.logback.classic.{Level, LoggerContext} +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import org.slf4j.LoggerFactory + +import scala.concurrent.duration._ + +/** + * Performance test for the Comment entity. + */ +class CommentGatlingTest extends Simulation { + + val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + // Log all HTTP requests + //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) + // Log failed HTTP requests + //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) + + val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://127.0.0.1:8080""" + + val httpConf = http + .baseURL(baseURL) + .inferHtmlResources() + .acceptHeader("*/*") + .acceptEncodingHeader("gzip, deflate") + .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") + .connectionHeader("keep-alive") + .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") + + val headers_http = Map( + "Accept" -> """application/json""" + ) + + val headers_http_authentication = Map( + "Content-Type" -> """application/json""", + "Accept" -> """application/json""" + ) + + val headers_http_authenticated = Map( + "Accept" -> """application/json""", + "Authorization" -> "${access_token}" + ) + + val scn = scenario("Test the Comment entity") + .exec(http("First unauthenticated request") + .get("/api/account") + .headers(headers_http) + .check(status.is(401))).exitHereIfFailed + .pause(10) + .exec(http("Authentication") + .post("/api/authenticate") + .headers(headers_http_authentication) + .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJSON + .check(header.get("Authorization").saveAs("access_token"))).exitHereIfFailed + .pause(1) + .exec(http("Authenticated request") + .get("/api/account") + .headers(headers_http_authenticated) + .check(status.is(200))) + .pause(10) + .repeat(2) { + exec(http("Get all comments") + .get("/api/comments") + .headers(headers_http_authenticated) + .check(status.is(200))) + .pause(10 seconds, 20 seconds) + .exec(http("Create new comment") + .post("/api/comments") + .headers(headers_http_authenticated) + .body(StringBody("""{"id":null, "text":"SAMPLE_TEXT", "creationDate":"2020-01-01T00:00:00.000Z"}""")).asJSON + .check(status.is(201)) + .check(headerRegex("Location", "(.*)").saveAs("new_comment_url"))).exitHereIfFailed + .pause(10) + .repeat(5) { + exec(http("Get created comment") + .get("${new_comment_url}") + .headers(headers_http_authenticated)) + .pause(10) + } + .exec(http("Delete created comment") + .delete("${new_comment_url}") + .headers(headers_http_authenticated)) + .pause(10) + } + + val users = scenario("Users").exec(scn) + + setUp( + users.inject(rampUsers(100) over (1 minutes)) + ).protocols(httpConf) +} diff --git a/jhipster/src/test/gatling/simulations/PostGatlingTest.scala b/jhipster/src/test/gatling/simulations/PostGatlingTest.scala new file mode 100644 index 0000000000..d76198c9ae --- /dev/null +++ b/jhipster/src/test/gatling/simulations/PostGatlingTest.scala @@ -0,0 +1,92 @@ +import _root_.io.gatling.core.scenario.Simulation +import ch.qos.logback.classic.{Level, LoggerContext} +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import org.slf4j.LoggerFactory + +import scala.concurrent.duration._ + +/** + * Performance test for the Post entity. + */ +class PostGatlingTest extends Simulation { + + val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + // Log all HTTP requests + //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) + // Log failed HTTP requests + //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) + + val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://127.0.0.1:8080""" + + val httpConf = http + .baseURL(baseURL) + .inferHtmlResources() + .acceptHeader("*/*") + .acceptEncodingHeader("gzip, deflate") + .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") + .connectionHeader("keep-alive") + .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") + + val headers_http = Map( + "Accept" -> """application/json""" + ) + + val headers_http_authentication = Map( + "Content-Type" -> """application/json""", + "Accept" -> """application/json""" + ) + + val headers_http_authenticated = Map( + "Accept" -> """application/json""", + "Authorization" -> "${access_token}" + ) + + val scn = scenario("Test the Post entity") + .exec(http("First unauthenticated request") + .get("/api/account") + .headers(headers_http) + .check(status.is(401))).exitHereIfFailed + .pause(10) + .exec(http("Authentication") + .post("/api/authenticate") + .headers(headers_http_authentication) + .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJSON + .check(header.get("Authorization").saveAs("access_token"))).exitHereIfFailed + .pause(1) + .exec(http("Authenticated request") + .get("/api/account") + .headers(headers_http_authenticated) + .check(status.is(200))) + .pause(10) + .repeat(2) { + exec(http("Get all posts") + .get("/api/posts") + .headers(headers_http_authenticated) + .check(status.is(200))) + .pause(10 seconds, 20 seconds) + .exec(http("Create new post") + .post("/api/posts") + .headers(headers_http_authenticated) + .body(StringBody("""{"id":null, "title":"SAMPLE_TEXT", "content":"SAMPLE_TEXT", "creationDate":"2020-01-01T00:00:00.000Z"}""")).asJSON + .check(status.is(201)) + .check(headerRegex("Location", "(.*)").saveAs("new_post_url"))).exitHereIfFailed + .pause(10) + .repeat(5) { + exec(http("Get created post") + .get("${new_post_url}") + .headers(headers_http_authenticated)) + .pause(10) + } + .exec(http("Delete created post") + .delete("${new_post_url}") + .headers(headers_http_authenticated)) + .pause(10) + } + + val users = scenario("Users").exec(scn) + + setUp( + users.inject(rampUsers(100) over (1 minutes)) + ).protocols(httpConf) +} diff --git a/jhipster/src/test/java/com/baeldung/security/SecurityUtilsUnitTest.java b/jhipster/src/test/java/com/baeldung/security/SecurityUtilsUnitTest.java new file mode 100644 index 0000000000..b78a18790b --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/security/SecurityUtilsUnitTest.java @@ -0,0 +1,50 @@ +package com.baeldung.security; + +import org.junit.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.ArrayList; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +/** +* Test class for the SecurityUtils utility class. +* +* @see SecurityUtils +*/ +public class SecurityUtilsUnitTest { + + @Test + public void testgetCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin")); + SecurityContextHolder.setContext(securityContext); + String login = SecurityUtils.getCurrentUserLogin(); + assertThat(login).isEqualTo("admin"); + } + + @Test + public void testIsAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin")); + SecurityContextHolder.setContext(securityContext); + boolean isAuthenticated = SecurityUtils.isAuthenticated(); + assertThat(isAuthenticated).isTrue(); + } + + @Test + public void testAnonymousIsNotAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("anonymous", "anonymous", authorities)); + SecurityContextHolder.setContext(securityContext); + boolean isAuthenticated = SecurityUtils.isAuthenticated(); + assertThat(isAuthenticated).isFalse(); + } +} diff --git a/jhipster/src/test/java/com/baeldung/security/jwt/TokenProviderTest.java b/jhipster/src/test/java/com/baeldung/security/jwt/TokenProviderTest.java new file mode 100644 index 0000000000..3fec4bfb88 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/security/jwt/TokenProviderTest.java @@ -0,0 +1,107 @@ +package com.baeldung.security.jwt; + +import com.baeldung.security.AuthoritiesConstants; +import io.github.jhipster.config.JHipsterProperties; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TokenProviderTest { + + private final String secretKey = "e5c9ee274ae87bc031adda32e27fa98b9290da83"; + private final long ONE_MINUTE = 60000; + private JHipsterProperties jHipsterProperties; + private TokenProvider tokenProvider; + + @Before + public void setup() { + jHipsterProperties = Mockito.mock(JHipsterProperties.class); + tokenProvider = new TokenProvider(jHipsterProperties); + ReflectionTestUtils.setField(tokenProvider, "secretKey", secretKey); + ReflectionTestUtils.setField(tokenProvider, "tokenValidityInMilliseconds", ONE_MINUTE); + } + + @Test + public void testReturnFalseWhenJWThasInvalidSignature() { + boolean isTokenValid = tokenProvider.validateToken(createTokenWithDifferentSignature()); + + assertThat(isTokenValid).isEqualTo(false); + } + + @Test + public void testReturnFalseWhenJWTisMalformed() { + Authentication authentication = createAuthentication(); + String token = tokenProvider.createToken(authentication, false); + String invalidToken = token.substring(1); + boolean isTokenValid = tokenProvider.validateToken(invalidToken); + + assertThat(isTokenValid).isEqualTo(false); + } + + @Test + public void testReturnFalseWhenJWTisExpired() { + ReflectionTestUtils.setField(tokenProvider, "tokenValidityInMilliseconds", -ONE_MINUTE); + + Authentication authentication = createAuthentication(); + String token = tokenProvider.createToken(authentication, false); + + boolean isTokenValid = tokenProvider.validateToken(token); + + assertThat(isTokenValid).isEqualTo(false); + } + + @Test + public void testReturnFalseWhenJWTisUnsupported() { + Date expirationDate = new Date(new Date().getTime() + ONE_MINUTE); + + Authentication authentication = createAuthentication(); + + String unsupportedToken = createUnsupportedToken(); + + boolean isTokenValid = tokenProvider.validateToken(unsupportedToken); + + assertThat(isTokenValid).isEqualTo(false); + } + + @Test + public void testReturnFalseWhenJWTisInvalid() { + + boolean isTokenValid = tokenProvider.validateToken(""); + + assertThat(isTokenValid).isEqualTo(false); + } + + private Authentication createAuthentication() { + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS)); + return new UsernamePasswordAuthenticationToken("anonymous", "anonymous", authorities); + } + + private String createUnsupportedToken() { + return Jwts.builder() + .setPayload("payload") + .signWith(SignatureAlgorithm.HS512, secretKey) + .compact(); + } + + private String createTokenWithDifferentSignature() { + return Jwts.builder() + .setSubject("anonymous") + .signWith(SignatureAlgorithm.HS512, "e5c9ee274ae87bc031adda32e27fa98b9290da90") + .setExpiration(new Date(new Date().getTime() + ONE_MINUTE)) + .compact(); + } +} diff --git a/jhipster/src/test/java/com/baeldung/service/UserServiceIntTest.java b/jhipster/src/test/java/com/baeldung/service/UserServiceIntTest.java new file mode 100644 index 0000000000..968f0a7f08 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/service/UserServiceIntTest.java @@ -0,0 +1,129 @@ +package com.baeldung.service; + +import com.baeldung.BaeldungApp; +import com.baeldung.domain.User; +import com.baeldung.config.Constants; +import com.baeldung.repository.UserRepository; +import com.baeldung.service.dto.UserDTO; +import java.time.ZonedDateTime; +import com.baeldung.service.util.RandomUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.test.context.junit4.SpringRunner; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import java.util.Optional; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +/** + * Test class for the UserResource REST controller. + * + * @see UserService + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +@Transactional +public class UserServiceIntTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Test + public void assertThatUserMustExistToResetPassword() { + Optional maybeUser = userService.requestPasswordReset("john.doe@localhost"); + assertThat(maybeUser.isPresent()).isFalse(); + + maybeUser = userService.requestPasswordReset("admin@localhost"); + assertThat(maybeUser.isPresent()).isTrue(); + + assertThat(maybeUser.get().getEmail()).isEqualTo("admin@localhost"); + assertThat(maybeUser.get().getResetDate()).isNotNull(); + assertThat(maybeUser.get().getResetKey()).isNotNull(); + } + + @Test + public void assertThatOnlyActivatedUserCanRequestPasswordReset() { + User user = userService.createUser("johndoe", "johndoe", "John", "Doe", "john.doe@localhost", "http://placehold.it/50x50", "en-US"); + Optional maybeUser = userService.requestPasswordReset("john.doe@localhost"); + assertThat(maybeUser.isPresent()).isFalse(); + userRepository.delete(user); + } + + @Test + public void assertThatResetKeyMustNotBeOlderThan24Hours() { + User user = userService.createUser("johndoe", "johndoe", "John", "Doe", "john.doe@localhost", "http://placehold.it/50x50", "en-US"); + + ZonedDateTime daysAgo = ZonedDateTime.now().minusHours(25); + String resetKey = RandomUtil.generateResetKey(); + user.setActivated(true); + user.setResetDate(daysAgo); + user.setResetKey(resetKey); + + userRepository.save(user); + + Optional maybeUser = userService.completePasswordReset("johndoe2", user.getResetKey()); + + assertThat(maybeUser.isPresent()).isFalse(); + + userRepository.delete(user); + } + + @Test + public void assertThatResetKeyMustBeValid() { + User user = userService.createUser("johndoe", "johndoe", "John", "Doe", "john.doe@localhost", "http://placehold.it/50x50", "en-US"); + + ZonedDateTime daysAgo = ZonedDateTime.now().minusHours(25); + user.setActivated(true); + user.setResetDate(daysAgo); + user.setResetKey("1234"); + userRepository.save(user); + Optional maybeUser = userService.completePasswordReset("johndoe2", user.getResetKey()); + assertThat(maybeUser.isPresent()).isFalse(); + userRepository.delete(user); + } + + @Test + public void assertThatUserCanResetPassword() { + User user = userService.createUser("johndoe", "johndoe", "John", "Doe", "john.doe@localhost", "http://placehold.it/50x50", "en-US"); + String oldPassword = user.getPassword(); + ZonedDateTime daysAgo = ZonedDateTime.now().minusHours(2); + String resetKey = RandomUtil.generateResetKey(); + user.setActivated(true); + user.setResetDate(daysAgo); + user.setResetKey(resetKey); + userRepository.save(user); + Optional maybeUser = userService.completePasswordReset("johndoe2", user.getResetKey()); + assertThat(maybeUser.isPresent()).isTrue(); + assertThat(maybeUser.get().getResetDate()).isNull(); + assertThat(maybeUser.get().getResetKey()).isNull(); + assertThat(maybeUser.get().getPassword()).isNotEqualTo(oldPassword); + + userRepository.delete(user); + } + + @Test + public void testFindNotActivatedUsersByCreationDateBefore() { + userService.removeNotActivatedUsers(); + ZonedDateTime now = ZonedDateTime.now(); + List users = userRepository.findAllByActivatedIsFalseAndCreatedDateBefore(now.minusDays(3)); + assertThat(users).isEmpty(); + } + + @Test + public void assertThatAnonymousUserIsNotGet() { + final PageRequest pageable = new PageRequest(0, (int) userRepository.count()); + final Page allManagedUsers = userService.getAllManagedUsers(pageable); + assertThat(allManagedUsers.getContent().stream() + .noneMatch(user -> Constants.ANONYMOUS_USER.equals(user.getLogin()))) + .isTrue(); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/AccountResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/AccountResourceIntTest.java new file mode 100644 index 0000000000..e42ce1c6d4 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/AccountResourceIntTest.java @@ -0,0 +1,395 @@ +package com.baeldung.web.rest; + +import com.baeldung.BaeldungApp; +import com.baeldung.domain.Authority; +import com.baeldung.domain.User; +import com.baeldung.repository.AuthorityRepository; +import com.baeldung.repository.UserRepository; +import com.baeldung.security.AuthoritiesConstants; +import com.baeldung.service.MailService; +import com.baeldung.service.UserService; +import com.baeldung.service.dto.UserDTO; +import com.baeldung.web.rest.vm.ManagedUserVM; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for the AccountResource REST controller. + * + * @see AccountResource + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +public class AccountResourceIntTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private AuthorityRepository authorityRepository; + + @Autowired + private UserService userService; + + @Mock + private UserService mockUserService; + + @Mock + private MailService mockMailService; + + private MockMvc restUserMockMvc; + + private MockMvc restMvc; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + doNothing().when(mockMailService).sendActivationEmail(anyObject()); + + AccountResource accountResource = + new AccountResource(userRepository, userService, mockMailService); + + AccountResource accountUserMockResource = + new AccountResource(userRepository, mockUserService, mockMailService); + + this.restMvc = MockMvcBuilders.standaloneSetup(accountResource).build(); + this.restUserMockMvc = MockMvcBuilders.standaloneSetup(accountUserMockResource).build(); + } + + @Test + public void testNonAuthenticatedUser() throws Exception { + restUserMockMvc.perform(get("/api/authenticate") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("")); + } + + @Test + public void testAuthenticatedUser() throws Exception { + restUserMockMvc.perform(get("/api/authenticate") + .with(request -> { + request.setRemoteUser("test"); + return request; + }) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("test")); + } + + @Test + public void testGetExistingAccount() throws Exception { + Set authorities = new HashSet<>(); + Authority authority = new Authority(); + authority.setName(AuthoritiesConstants.ADMIN); + authorities.add(authority); + + User user = new User(); + user.setLogin("test"); + user.setFirstName("john"); + user.setLastName("doe"); + user.setEmail("john.doe@jhipster.com"); + user.setImageUrl("http://placehold.it/50x50"); + user.setAuthorities(authorities); + when(mockUserService.getUserWithAuthorities()).thenReturn(user); + + restUserMockMvc.perform(get("/api/account") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.login").value("test")) + .andExpect(jsonPath("$.firstName").value("john")) + .andExpect(jsonPath("$.lastName").value("doe")) + .andExpect(jsonPath("$.email").value("john.doe@jhipster.com")) + .andExpect(jsonPath("$.imageUrl").value("http://placehold.it/50x50")) + .andExpect(jsonPath("$.authorities").value(AuthoritiesConstants.ADMIN)); + } + + @Test + public void testGetUnknownAccount() throws Exception { + when(mockUserService.getUserWithAuthorities()).thenReturn(null); + + restUserMockMvc.perform(get("/api/account") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isInternalServerError()); + } + + @Test + @Transactional + public void testRegisterValid() throws Exception { + ManagedUserVM validUser = new ManagedUserVM( + null, // id + "joe", // login + "password", // password + "Joe", // firstName + "Shmoe", // lastName + "joe@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER))); + + restMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(validUser))) + .andExpect(status().isCreated()); + + Optional user = userRepository.findOneByLogin("joe"); + assertThat(user.isPresent()).isTrue(); + } + + @Test + @Transactional + public void testRegisterInvalidLogin() throws Exception { + ManagedUserVM invalidUser = new ManagedUserVM( + null, // id + "funky-log!n", // login <-- invalid + "password", // password + "Funky", // firstName + "One", // lastName + "funky@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER))); + + restUserMockMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(invalidUser))) + .andExpect(status().isBadRequest()); + + Optional user = userRepository.findOneByEmail("funky@example.com"); + assertThat(user.isPresent()).isFalse(); + } + + @Test + @Transactional + public void testRegisterInvalidEmail() throws Exception { + ManagedUserVM invalidUser = new ManagedUserVM( + null, // id + "bob", // login + "password", // password + "Bob", // firstName + "Green", // lastName + "invalid", // e-mail <-- invalid + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER))); + + restUserMockMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(invalidUser))) + .andExpect(status().isBadRequest()); + + Optional user = userRepository.findOneByLogin("bob"); + assertThat(user.isPresent()).isFalse(); + } + + @Test + @Transactional + public void testRegisterInvalidPassword() throws Exception { + ManagedUserVM invalidUser = new ManagedUserVM( + null, // id + "bob", // login + "123", // password with only 3 digits + "Bob", // firstName + "Green", // lastName + "bob@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER))); + + restUserMockMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(invalidUser))) + .andExpect(status().isBadRequest()); + + Optional user = userRepository.findOneByLogin("bob"); + assertThat(user.isPresent()).isFalse(); + } + + @Test + @Transactional + public void testRegisterDuplicateLogin() throws Exception { + // Good + ManagedUserVM validUser = new ManagedUserVM( + null, // id + "alice", // login + "password", // password + "Alice", // firstName + "Something", // lastName + "alice@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER))); + + // Duplicate login, different e-mail + ManagedUserVM duplicatedUser = new ManagedUserVM(validUser.getId(), validUser.getLogin(), validUser.getPassword(), validUser.getLogin(), validUser.getLastName(), + "alicejr@example.com", true, validUser.getImageUrl(), validUser.getLangKey(), validUser.getCreatedBy(), validUser.getCreatedDate(), validUser.getLastModifiedBy(), validUser.getLastModifiedDate(), validUser.getAuthorities()); + + // Good user + restMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(validUser))) + .andExpect(status().isCreated()); + + // Duplicate login + restMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(duplicatedUser))) + .andExpect(status().is4xxClientError()); + + Optional userDup = userRepository.findOneByEmail("alicejr@example.com"); + assertThat(userDup.isPresent()).isFalse(); + } + + @Test + @Transactional + public void testRegisterDuplicateEmail() throws Exception { + // Good + ManagedUserVM validUser = new ManagedUserVM( + null, // id + "john", // login + "password", // password + "John", // firstName + "Doe", // lastName + "john@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER))); + + // Duplicate e-mail, different login + ManagedUserVM duplicatedUser = new ManagedUserVM(validUser.getId(), "johnjr", validUser.getPassword(), validUser.getLogin(), validUser.getLastName(), + validUser.getEmail(), true, validUser.getImageUrl(), validUser.getLangKey(), validUser.getCreatedBy(), validUser.getCreatedDate(), validUser.getLastModifiedBy(), validUser.getLastModifiedDate(), validUser.getAuthorities()); + + // Good user + restMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(validUser))) + .andExpect(status().isCreated()); + + // Duplicate e-mail + restMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(duplicatedUser))) + .andExpect(status().is4xxClientError()); + + Optional userDup = userRepository.findOneByLogin("johnjr"); + assertThat(userDup.isPresent()).isFalse(); + } + + @Test + @Transactional + public void testRegisterAdminIsIgnored() throws Exception { + ManagedUserVM validUser = new ManagedUserVM( + null, // id + "badguy", // login + "password", // password + "Bad", // firstName + "Guy", // lastName + "badguy@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.ADMIN))); + + restMvc.perform( + post("/api/register") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(validUser))) + .andExpect(status().isCreated()); + + Optional userDup = userRepository.findOneByLogin("badguy"); + assertThat(userDup.isPresent()).isTrue(); + assertThat(userDup.get().getAuthorities()).hasSize(1) + .containsExactly(authorityRepository.findOne(AuthoritiesConstants.USER)); + } + + @Test + @Transactional + public void testSaveInvalidLogin() throws Exception { + UserDTO invalidUser = new UserDTO( + null, // id + "funky-log!n", // login <-- invalid + "Funky", // firstName + "One", // lastName + "funky@example.com", // e-mail + true, // activated + "http://placehold.it/50x50", //imageUrl + "en", // langKey + null, // createdBy + null, // createdDate + null, // lastModifiedBy + null, // lastModifiedDate + new HashSet<>(Arrays.asList(AuthoritiesConstants.USER)) + ); + + restUserMockMvc.perform( + post("/api/account") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(invalidUser))) + .andExpect(status().isBadRequest()); + + Optional user = userRepository.findOneByEmail("funky@example.com"); + assertThat(user.isPresent()).isFalse(); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/AuditResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/AuditResourceIntTest.java new file mode 100644 index 0000000000..127cb36f07 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/AuditResourceIntTest.java @@ -0,0 +1,147 @@ +package com.baeldung.web.rest; + +import com.baeldung.BaeldungApp; +import com.baeldung.config.audit.AuditEventConverter; +import com.baeldung.domain.PersistentAuditEvent; +import com.baeldung.repository.PersistenceAuditEventRepository; +import com.baeldung.service.AuditEventService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for the AuditResource REST controller. + * + * @see AuditResource + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +@Transactional +public class AuditResourceIntTest { + + private static final String SAMPLE_PRINCIPAL = "SAMPLE_PRINCIPAL"; + private static final String SAMPLE_TYPE = "SAMPLE_TYPE"; + private static final LocalDateTime SAMPLE_TIMESTAMP = LocalDateTime.parse("2015-08-04T10:11:30"); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Autowired + private PersistenceAuditEventRepository auditEventRepository; + + @Autowired + private AuditEventConverter auditEventConverter; + + @Autowired + private MappingJackson2HttpMessageConverter jacksonMessageConverter; + + @Autowired + private FormattingConversionService formattingConversionService; + + @Autowired + private PageableHandlerMethodArgumentResolver pageableArgumentResolver; + + private PersistentAuditEvent auditEvent; + + private MockMvc restAuditMockMvc; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + AuditEventService auditEventService = + new AuditEventService(auditEventRepository, auditEventConverter); + AuditResource auditResource = new AuditResource(auditEventService); + this.restAuditMockMvc = MockMvcBuilders.standaloneSetup(auditResource) + .setCustomArgumentResolvers(pageableArgumentResolver) + .setConversionService(formattingConversionService) + .setMessageConverters(jacksonMessageConverter).build(); + } + + @Before + public void initTest() { + auditEventRepository.deleteAll(); + auditEvent = new PersistentAuditEvent(); + auditEvent.setAuditEventType(SAMPLE_TYPE); + auditEvent.setPrincipal(SAMPLE_PRINCIPAL); + auditEvent.setAuditEventDate(SAMPLE_TIMESTAMP); + } + + @Test + public void getAllAudits() throws Exception { + // Initialize the database + auditEventRepository.save(auditEvent); + + // Get all the audits + restAuditMockMvc.perform(get("/management/audits")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.[*].principal").value(hasItem(SAMPLE_PRINCIPAL))); + } + + @Test + public void getAudit() throws Exception { + // Initialize the database + auditEventRepository.save(auditEvent); + + // Get the audit + restAuditMockMvc.perform(get("/management/audits/{id}", auditEvent.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.principal").value(SAMPLE_PRINCIPAL)); + } + + @Test + public void getAuditsByDate() throws Exception { + // Initialize the database + auditEventRepository.save(auditEvent); + + // Generate dates for selecting audits by date, making sure the period will contain the audit + String fromDate = SAMPLE_TIMESTAMP.minusDays(1).format(FORMATTER); + String toDate = SAMPLE_TIMESTAMP.plusDays(1).format(FORMATTER); + + // Get the audit + restAuditMockMvc.perform(get("/management/audits?fromDate="+fromDate+"&toDate="+toDate)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.[*].principal").value(hasItem(SAMPLE_PRINCIPAL))); + } + + @Test + public void getNonExistingAuditsByDate() throws Exception { + // Initialize the database + auditEventRepository.save(auditEvent); + + // Generate dates for selecting audits by date, making sure the period will not contain the sample audit + String fromDate = SAMPLE_TIMESTAMP.minusDays(2).format(FORMATTER); + String toDate = SAMPLE_TIMESTAMP.minusDays(1).format(FORMATTER); + + // Query audits but expect no results + restAuditMockMvc.perform(get("/management/audits?fromDate=" + fromDate + "&toDate=" + toDate)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(header().string("X-Total-Count", "0")); + } + + @Test + public void getNonExistingAudit() throws Exception { + // Get the audit + restAuditMockMvc.perform(get("/management/audits/{id}", Long.MAX_VALUE)) + .andExpect(status().isNotFound()); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/CommentResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/CommentResourceIntTest.java new file mode 100644 index 0000000000..04b16b25f8 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/CommentResourceIntTest.java @@ -0,0 +1,279 @@ +package com.baeldung.web.rest; + +import com.baeldung.BaeldungApp; + +import com.baeldung.domain.Comment; +import com.baeldung.domain.Post; +import com.baeldung.repository.CommentRepository; +import com.baeldung.web.rest.errors.ExceptionTranslator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for the CommentResource REST controller. + * + * @see CommentResource + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +public class CommentResourceIntTest { + + private static final String DEFAULT_TEXT = "AAAAAAAAAA"; + private static final String UPDATED_TEXT = "BBBBBBBBBB"; + + private static final LocalDate DEFAULT_CREATION_DATE = LocalDate.ofEpochDay(0L); + private static final LocalDate UPDATED_CREATION_DATE = LocalDate.now(ZoneId.systemDefault()); + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MappingJackson2HttpMessageConverter jacksonMessageConverter; + + @Autowired + private PageableHandlerMethodArgumentResolver pageableArgumentResolver; + + @Autowired + private ExceptionTranslator exceptionTranslator; + + @Autowired + private EntityManager em; + + private MockMvc restCommentMockMvc; + + private Comment comment; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + CommentResource commentResource = new CommentResource(commentRepository); + this.restCommentMockMvc = MockMvcBuilders.standaloneSetup(commentResource) + .setCustomArgumentResolvers(pageableArgumentResolver) + .setControllerAdvice(exceptionTranslator) + .setMessageConverters(jacksonMessageConverter).build(); + } + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Comment createEntity(EntityManager em) { + Comment comment = new Comment() + .text(DEFAULT_TEXT) + .creationDate(DEFAULT_CREATION_DATE); + // Add required entity + Post post = PostResourceIntTest.createEntity(em); + em.persist(post); + em.flush(); + comment.setPost(post); + return comment; + } + + @Before + public void initTest() { + comment = createEntity(em); + } + + @Test + @Transactional + public void createComment() throws Exception { + int databaseSizeBeforeCreate = commentRepository.findAll().size(); + + // Create the Comment + restCommentMockMvc.perform(post("/api/comments") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(comment))) + .andExpect(status().isCreated()); + + // Validate the Comment in the database + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeCreate + 1); + Comment testComment = commentList.get(commentList.size() - 1); + assertThat(testComment.getText()).isEqualTo(DEFAULT_TEXT); + assertThat(testComment.getCreationDate()).isEqualTo(DEFAULT_CREATION_DATE); + } + + @Test + @Transactional + public void createCommentWithExistingId() throws Exception { + int databaseSizeBeforeCreate = commentRepository.findAll().size(); + + // Create the Comment with an existing ID + comment.setId(1L); + + // An entity with an existing ID cannot be created, so this API call must fail + restCommentMockMvc.perform(post("/api/comments") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(comment))) + .andExpect(status().isBadRequest()); + + // Validate the Alice in the database + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeCreate); + } + + @Test + @Transactional + public void checkTextIsRequired() throws Exception { + int databaseSizeBeforeTest = commentRepository.findAll().size(); + // set the field null + comment.setText(null); + + // Create the Comment, which fails. + + restCommentMockMvc.perform(post("/api/comments") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(comment))) + .andExpect(status().isBadRequest()); + + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeTest); + } + + @Test + @Transactional + public void checkCreationDateIsRequired() throws Exception { + int databaseSizeBeforeTest = commentRepository.findAll().size(); + // set the field null + comment.setCreationDate(null); + + // Create the Comment, which fails. + + restCommentMockMvc.perform(post("/api/comments") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(comment))) + .andExpect(status().isBadRequest()); + + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeTest); + } + + @Test + @Transactional + public void getAllComments() throws Exception { + // Initialize the database + commentRepository.saveAndFlush(comment); + + // Get all the commentList + restCommentMockMvc.perform(get("/api/comments?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(comment.getId().intValue()))) + .andExpect(jsonPath("$.[*].text").value(hasItem(DEFAULT_TEXT.toString()))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))); + } + + @Test + @Transactional + public void getComment() throws Exception { + // Initialize the database + commentRepository.saveAndFlush(comment); + + // Get the comment + restCommentMockMvc.perform(get("/api/comments/{id}", comment.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.id").value(comment.getId().intValue())) + .andExpect(jsonPath("$.text").value(DEFAULT_TEXT.toString())) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())); + } + + @Test + @Transactional + public void getNonExistingComment() throws Exception { + // Get the comment + restCommentMockMvc.perform(get("/api/comments/{id}", Long.MAX_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + @Transactional + public void updateComment() throws Exception { + // Initialize the database + commentRepository.saveAndFlush(comment); + int databaseSizeBeforeUpdate = commentRepository.findAll().size(); + + // Update the comment + Comment updatedComment = commentRepository.findOne(comment.getId()); + updatedComment + .text(UPDATED_TEXT) + .creationDate(UPDATED_CREATION_DATE); + + restCommentMockMvc.perform(put("/api/comments") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(updatedComment))) + .andExpect(status().isOk()); + + // Validate the Comment in the database + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeUpdate); + Comment testComment = commentList.get(commentList.size() - 1); + assertThat(testComment.getText()).isEqualTo(UPDATED_TEXT); + assertThat(testComment.getCreationDate()).isEqualTo(UPDATED_CREATION_DATE); + } + + @Test + @Transactional + public void updateNonExistingComment() throws Exception { + int databaseSizeBeforeUpdate = commentRepository.findAll().size(); + + // Create the Comment + + // If the entity doesn't have an ID, it will be created instead of just being updated + restCommentMockMvc.perform(put("/api/comments") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(comment))) + .andExpect(status().isCreated()); + + // Validate the Comment in the database + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeUpdate + 1); + } + + @Test + @Transactional + public void deleteComment() throws Exception { + // Initialize the database + commentRepository.saveAndFlush(comment); + int databaseSizeBeforeDelete = commentRepository.findAll().size(); + + // Get the comment + restCommentMockMvc.perform(delete("/api/comments/{id}", comment.getId()) + .accept(TestUtil.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()); + + // Validate the database is empty + List commentList = commentRepository.findAll(); + assertThat(commentList).hasSize(databaseSizeBeforeDelete - 1); + } + + @Test + @Transactional + public void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Comment.class); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/LogsResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/LogsResourceIntTest.java new file mode 100644 index 0000000000..92bb976205 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/LogsResourceIntTest.java @@ -0,0 +1,59 @@ +package com.baeldung.web.rest; + +import com.baeldung.BaeldungApp; +import com.baeldung.web.rest.vm.LoggerVM; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Test class for the LogsResource REST controller. + * + * @see LogsResource + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +public class LogsResourceIntTest { + + private MockMvc restLogsMockMvc; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + LogsResource logsResource = new LogsResource(); + this.restLogsMockMvc = MockMvcBuilders + .standaloneSetup(logsResource) + .build(); + } + + @Test + public void getAllLogs()throws Exception { + restLogsMockMvc.perform(get("/management/logs")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)); + } + + @Test + public void changeLogs()throws Exception { + LoggerVM logger = new LoggerVM(); + logger.setLevel("INFO"); + logger.setName("ROOT"); + + restLogsMockMvc.perform(put("/management/logs") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(logger))) + .andExpect(status().isNoContent()); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/PostResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/PostResourceIntTest.java new file mode 100644 index 0000000000..2a23452711 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/PostResourceIntTest.java @@ -0,0 +1,306 @@ +package com.baeldung.web.rest; + +import com.baeldung.BaeldungApp; + +import com.baeldung.domain.Post; +import com.baeldung.domain.User; +import com.baeldung.repository.PostRepository; +import com.baeldung.web.rest.errors.ExceptionTranslator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for the PostResource REST controller. + * + * @see PostResource + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +public class PostResourceIntTest { + + private static final String DEFAULT_TITLE = "AAAAAAAAAA"; + private static final String UPDATED_TITLE = "BBBBBBBBBB"; + + private static final String DEFAULT_CONTENT = "AAAAAAAAAA"; + private static final String UPDATED_CONTENT = "BBBBBBBBBB"; + + private static final LocalDate DEFAULT_CREATION_DATE = LocalDate.ofEpochDay(0L); + private static final LocalDate UPDATED_CREATION_DATE = LocalDate.now(ZoneId.systemDefault()); + + @Autowired + private PostRepository postRepository; + + @Autowired + private MappingJackson2HttpMessageConverter jacksonMessageConverter; + + @Autowired + private PageableHandlerMethodArgumentResolver pageableArgumentResolver; + + @Autowired + private ExceptionTranslator exceptionTranslator; + + @Autowired + private EntityManager em; + + private MockMvc restPostMockMvc; + + private Post post; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + PostResource postResource = new PostResource(postRepository); + this.restPostMockMvc = MockMvcBuilders.standaloneSetup(postResource) + .setCustomArgumentResolvers(pageableArgumentResolver) + .setControllerAdvice(exceptionTranslator) + .setMessageConverters(jacksonMessageConverter).build(); + } + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Post createEntity(EntityManager em) { + Post post = new Post() + .title(DEFAULT_TITLE) + .content(DEFAULT_CONTENT) + .creationDate(DEFAULT_CREATION_DATE); + // Add required entity + User creator = UserResourceIntTest.createEntity(em); + em.persist(creator); + em.flush(); + post.setCreator(creator); + return post; + } + + @Before + public void initTest() { + post = createEntity(em); + } + + @Test + @Transactional + public void createPost() throws Exception { + int databaseSizeBeforeCreate = postRepository.findAll().size(); + + // Create the Post + restPostMockMvc.perform(post("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(post))) + .andExpect(status().isCreated()); + + // Validate the Post in the database + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeCreate + 1); + Post testPost = postList.get(postList.size() - 1); + assertThat(testPost.getTitle()).isEqualTo(DEFAULT_TITLE); + assertThat(testPost.getContent()).isEqualTo(DEFAULT_CONTENT); + assertThat(testPost.getCreationDate()).isEqualTo(DEFAULT_CREATION_DATE); + } + + @Test + @Transactional + public void createPostWithExistingId() throws Exception { + int databaseSizeBeforeCreate = postRepository.findAll().size(); + + // Create the Post with an existing ID + post.setId(1L); + + // An entity with an existing ID cannot be created, so this API call must fail + restPostMockMvc.perform(post("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(post))) + .andExpect(status().isBadRequest()); + + // Validate the Alice in the database + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeCreate); + } + + @Test + @Transactional + public void checkTitleIsRequired() throws Exception { + int databaseSizeBeforeTest = postRepository.findAll().size(); + // set the field null + post.setTitle(null); + + // Create the Post, which fails. + + restPostMockMvc.perform(post("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(post))) + .andExpect(status().isBadRequest()); + + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeTest); + } + + @Test + @Transactional + public void checkContentIsRequired() throws Exception { + int databaseSizeBeforeTest = postRepository.findAll().size(); + // set the field null + post.setContent(null); + + // Create the Post, which fails. + + restPostMockMvc.perform(post("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(post))) + .andExpect(status().isBadRequest()); + + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeTest); + } + + @Test + @Transactional + public void checkCreationDateIsRequired() throws Exception { + int databaseSizeBeforeTest = postRepository.findAll().size(); + // set the field null + post.setCreationDate(null); + + // Create the Post, which fails. + + restPostMockMvc.perform(post("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(post))) + .andExpect(status().isBadRequest()); + + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeTest); + } + + @Test + @Transactional + public void getAllPosts() throws Exception { + // Initialize the database + postRepository.saveAndFlush(post); + + // Get all the postList + restPostMockMvc.perform(get("/api/posts?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(post.getId().intValue()))) + .andExpect(jsonPath("$.[*].title").value(hasItem(DEFAULT_TITLE.toString()))) + .andExpect(jsonPath("$.[*].content").value(hasItem(DEFAULT_CONTENT.toString()))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))); + } + + @Test + @Transactional + public void getPost() throws Exception { + // Initialize the database + postRepository.saveAndFlush(post); + + // Get the post + restPostMockMvc.perform(get("/api/posts/{id}", post.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.id").value(post.getId().intValue())) + .andExpect(jsonPath("$.title").value(DEFAULT_TITLE.toString())) + .andExpect(jsonPath("$.content").value(DEFAULT_CONTENT.toString())) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())); + } + + @Test + @Transactional + public void getNonExistingPost() throws Exception { + // Get the post + restPostMockMvc.perform(get("/api/posts/{id}", Long.MAX_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + @Transactional + public void updatePost() throws Exception { + // Initialize the database + postRepository.saveAndFlush(post); + int databaseSizeBeforeUpdate = postRepository.findAll().size(); + + // Update the post + Post updatedPost = postRepository.findOne(post.getId()); + updatedPost + .title(UPDATED_TITLE) + .content(UPDATED_CONTENT) + .creationDate(UPDATED_CREATION_DATE); + + restPostMockMvc.perform(put("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(updatedPost))) + .andExpect(status().isOk()); + + // Validate the Post in the database + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeUpdate); + Post testPost = postList.get(postList.size() - 1); + assertThat(testPost.getTitle()).isEqualTo(UPDATED_TITLE); + assertThat(testPost.getContent()).isEqualTo(UPDATED_CONTENT); + assertThat(testPost.getCreationDate()).isEqualTo(UPDATED_CREATION_DATE); + } + + @Test + @Transactional + public void updateNonExistingPost() throws Exception { + int databaseSizeBeforeUpdate = postRepository.findAll().size(); + + // Create the Post + + // If the entity doesn't have an ID, it will be created instead of just being updated + restPostMockMvc.perform(put("/api/posts") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(post))) + .andExpect(status().isCreated()); + + // Validate the Post in the database + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeUpdate + 1); + } + + @Test + @Transactional + public void deletePost() throws Exception { + // Initialize the database + postRepository.saveAndFlush(post); + int databaseSizeBeforeDelete = postRepository.findAll().size(); + + // Get the post + restPostMockMvc.perform(delete("/api/posts/{id}", post.getId()) + .accept(TestUtil.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()); + + // Validate the database is empty + List postList = postRepository.findAll(); + assertThat(postList).hasSize(databaseSizeBeforeDelete - 1); + } + + @Test + @Transactional + public void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Post.class); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/ProfileInfoResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/ProfileInfoResourceIntTest.java new file mode 100644 index 0000000000..df3544f344 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/ProfileInfoResourceIntTest.java @@ -0,0 +1,86 @@ +package com.baeldung.web.rest; + +import io.github.jhipster.config.JHipsterProperties; +import com.baeldung.BaeldungApp; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Test class for the ProfileInfoResource REST controller. + * + * @see ProfileInfoResource + **/ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +public class ProfileInfoResourceIntTest { + + @Mock + private Environment environment; + + @Mock + private JHipsterProperties jHipsterProperties; + + private MockMvc restProfileMockMvc; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + String mockProfile[] = {"test"}; + JHipsterProperties.Ribbon ribbon = new JHipsterProperties.Ribbon(); + ribbon.setDisplayOnActiveProfiles(mockProfile); + when(jHipsterProperties.getRibbon()).thenReturn(ribbon); + + String activeProfiles[] = {"test"}; + when(environment.getDefaultProfiles()).thenReturn(activeProfiles); + when(environment.getActiveProfiles()).thenReturn(activeProfiles); + + ProfileInfoResource profileInfoResource = new ProfileInfoResource(environment, jHipsterProperties); + this.restProfileMockMvc = MockMvcBuilders + .standaloneSetup(profileInfoResource) + .build(); + } + + @Test + public void getProfileInfoWithRibbon() throws Exception { + restProfileMockMvc.perform(get("/api/profile-info")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)); + } + + @Test + public void getProfileInfoWithoutRibbon() throws Exception { + JHipsterProperties.Ribbon ribbon = new JHipsterProperties.Ribbon(); + ribbon.setDisplayOnActiveProfiles(null); + when(jHipsterProperties.getRibbon()).thenReturn(ribbon); + + restProfileMockMvc.perform(get("/api/profile-info")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)); + } + + @Test + public void getProfileInfoWithoutActiveProfiles() throws Exception { + String emptyProfile[] = {}; + when(environment.getDefaultProfiles()).thenReturn(emptyProfile); + when(environment.getActiveProfiles()).thenReturn(emptyProfile); + + restProfileMockMvc.perform(get("/api/profile-info")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/TestUtil.java b/jhipster/src/test/java/com/baeldung/web/rest/TestUtil.java new file mode 100644 index 0000000000..64d092fdf1 --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/TestUtil.java @@ -0,0 +1,120 @@ +package com.baeldung.web.rest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.http.MediaType; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Utility class for testing REST controllers. + */ +public class TestUtil { + + /** MediaType for JSON UTF8 */ + public static final MediaType APPLICATION_JSON_UTF8 = new MediaType( + MediaType.APPLICATION_JSON.getType(), + MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); + + /** + * Convert an object to JSON byte array. + * + * @param object + * the object to convert + * @return the JSON byte array + * @throws IOException + */ + public static byte[] convertObjectToJsonBytes(Object object) + throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + JavaTimeModule module = new JavaTimeModule(); + mapper.registerModule(module); + + return mapper.writeValueAsBytes(object); + } + + /** + * Create a byte array with a specific size filled with specified data. + * + * @param size the size of the byte array + * @param data the data to put in the byte array + * @return the JSON byte array + */ + public static byte[] createByteArray(int size, String data) { + byte[] byteArray = new byte[size]; + for (int i = 0; i < size; i++) { + byteArray[i] = Byte.parseByte(data, 2); + } + return byteArray; + } + + /** + * A matcher that tests that the examined string represents the same instant as the reference datetime. + */ + public static class ZonedDateTimeMatcher extends TypeSafeDiagnosingMatcher { + + private final ZonedDateTime date; + + public ZonedDateTimeMatcher(ZonedDateTime date) { + this.date = date; + } + + @Override + protected boolean matchesSafely(String item, Description mismatchDescription) { + try { + if (!date.isEqual(ZonedDateTime.parse(item))) { + mismatchDescription.appendText("was ").appendValue(item); + return false; + } + return true; + } catch (DateTimeParseException e) { + mismatchDescription.appendText("was ").appendValue(item) + .appendText(", which could not be parsed as a ZonedDateTime"); + return false; + } + + } + + @Override + public void describeTo(Description description) { + description.appendText("a String representing the same Instant as ").appendValue(date); + } + } + + /** + * Creates a matcher that matches when the examined string reprensents the same instant as the reference datetime + * @param date the reference datetime against which the examined string is checked + */ + public static ZonedDateTimeMatcher sameInstant(ZonedDateTime date) { + return new ZonedDateTimeMatcher(date); + } + + /** + * Verifies the equals/hashcode contract on the domain object. + */ + public static void equalsVerifier(Class clazz) throws Exception { + Object domainObject1 = clazz.getConstructor().newInstance(); + assertThat(domainObject1.toString()).isNotNull(); + assertThat(domainObject1).isEqualTo(domainObject1); + assertThat(domainObject1.hashCode()).isEqualTo(domainObject1.hashCode()); + // Test with an instance of another class + Object testOtherObject = new Object(); + assertThat(domainObject1).isNotEqualTo(testOtherObject); + // Test with an instance of the same class + Object domainObject2 = clazz.getConstructor().newInstance(); + assertThat(domainObject1).isNotEqualTo(domainObject2); + // HashCodes are equals because the objects are not persisted yet + assertThat(domainObject1.hashCode()).isEqualTo(domainObject2.hashCode()); + } +} diff --git a/jhipster/src/test/java/com/baeldung/web/rest/UserResourceIntTest.java b/jhipster/src/test/java/com/baeldung/web/rest/UserResourceIntTest.java new file mode 100644 index 0000000000..74df23283a --- /dev/null +++ b/jhipster/src/test/java/com/baeldung/web/rest/UserResourceIntTest.java @@ -0,0 +1,522 @@ +package com.baeldung.web.rest; + +import com.baeldung.BaeldungApp; +import com.baeldung.domain.User; +import com.baeldung.repository.UserRepository; +import com.baeldung.service.MailService; +import com.baeldung.service.UserService; +import com.baeldung.web.rest.errors.ExceptionTranslator; +import com.baeldung.web.rest.vm.ManagedUserVM; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for the UserResource REST controller. + * + * @see UserResource + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BaeldungApp.class) +public class UserResourceIntTest { + + private static final String DEFAULT_LOGIN = "johndoe"; + private static final String UPDATED_LOGIN = "jhipster"; + + private static final String DEFAULT_PASSWORD = "passjohndoe"; + private static final String UPDATED_PASSWORD = "passjhipster"; + + private static final String DEFAULT_EMAIL = "johndoe@localhost"; + private static final String UPDATED_EMAIL = "jhipster@localhost"; + + private static final String DEFAULT_FIRSTNAME = "john"; + private static final String UPDATED_FIRSTNAME = "jhipsterFirstName"; + + private static final String DEFAULT_LASTNAME = "doe"; + private static final String UPDATED_LASTNAME = "jhipsterLastName"; + + private static final String DEFAULT_IMAGEURL = "http://placehold.it/50x50"; + private static final String UPDATED_IMAGEURL = "http://placehold.it/40x40"; + + private static final String DEFAULT_LANGKEY = "en"; + private static final String UPDATED_LANGKEY = "fr"; + + @Autowired + private UserRepository userRepository; + + @Autowired + private MailService mailService; + + @Autowired + private UserService userService; + + @Autowired + private MappingJackson2HttpMessageConverter jacksonMessageConverter; + + @Autowired + private PageableHandlerMethodArgumentResolver pageableArgumentResolver; + + @Autowired + private ExceptionTranslator exceptionTranslator; + + @Autowired + private EntityManager em; + + private MockMvc restUserMockMvc; + + private User user; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + UserResource userResource = new UserResource(userRepository, mailService, userService); + this.restUserMockMvc = MockMvcBuilders.standaloneSetup(userResource) + .setCustomArgumentResolvers(pageableArgumentResolver) + .setControllerAdvice(exceptionTranslator) + .setMessageConverters(jacksonMessageConverter) + .build(); + } + + /** + * Create a User. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which has a required relationship to the User entity. + */ + public static User createEntity(EntityManager em) { + User user = new User(); + user.setLogin(DEFAULT_LOGIN); + user.setPassword(RandomStringUtils.random(60)); + user.setActivated(true); + user.setEmail(DEFAULT_EMAIL); + user.setFirstName(DEFAULT_FIRSTNAME); + user.setLastName(DEFAULT_LASTNAME); + user.setImageUrl(DEFAULT_IMAGEURL); + user.setLangKey(DEFAULT_LANGKEY); + return user; + } + + @Before + public void initTest() { + user = createEntity(em); + } + + @Test + @Transactional + public void createUser() throws Exception { + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + // Create the User + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + null, + DEFAULT_LOGIN, + DEFAULT_PASSWORD, + DEFAULT_FIRSTNAME, + DEFAULT_LASTNAME, + DEFAULT_EMAIL, + true, + DEFAULT_IMAGEURL, + DEFAULT_LANGKEY, + null, + null, + null, + null, + autorities); + + restUserMockMvc.perform(post("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isCreated()); + + // Validate the User in the database + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeCreate + 1); + User testUser = userList.get(userList.size() - 1); + assertThat(testUser.getLogin()).isEqualTo(DEFAULT_LOGIN); + assertThat(testUser.getFirstName()).isEqualTo(DEFAULT_FIRSTNAME); + assertThat(testUser.getLastName()).isEqualTo(DEFAULT_LASTNAME); + assertThat(testUser.getEmail()).isEqualTo(DEFAULT_EMAIL); + assertThat(testUser.getImageUrl()).isEqualTo(DEFAULT_IMAGEURL); + assertThat(testUser.getLangKey()).isEqualTo(DEFAULT_LANGKEY); + } + + @Test + @Transactional + public void createUserWithExistingId() throws Exception { + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + 1L, + DEFAULT_LOGIN, + DEFAULT_PASSWORD, + DEFAULT_FIRSTNAME, + DEFAULT_LASTNAME, + DEFAULT_EMAIL, + true, + DEFAULT_IMAGEURL, + DEFAULT_LANGKEY, + null, + null, + null, + null, + autorities); + + // An entity with an existing ID cannot be created, so this API call must fail + restUserMockMvc.perform(post("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isBadRequest()); + + // Validate the User in the database + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeCreate); + } + + @Test + @Transactional + public void createUserWithExistingLogin() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + null, + DEFAULT_LOGIN, // this login should already be used + DEFAULT_PASSWORD, + DEFAULT_FIRSTNAME, + DEFAULT_LASTNAME, + "anothermail@localhost", + true, + DEFAULT_IMAGEURL, + DEFAULT_LANGKEY, + null, + null, + null, + null, + autorities); + + // Create the User + restUserMockMvc.perform(post("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isBadRequest()); + + // Validate the User in the database + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeCreate); + } + + @Test + @Transactional + public void createUserWithExistingEmail() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + null, + "anotherlogin", + DEFAULT_PASSWORD, + DEFAULT_FIRSTNAME, + DEFAULT_LASTNAME, + DEFAULT_EMAIL, // this email should already be used + true, + DEFAULT_IMAGEURL, + DEFAULT_LANGKEY, + null, + null, + null, + null, + autorities); + + // Create the User + restUserMockMvc.perform(post("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isBadRequest()); + + // Validate the User in the database + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeCreate); + } + + @Test + @Transactional + public void getAllUsers() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + // Get all the users + restUserMockMvc.perform(get("/api/users?sort=id,desc") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.[*].login").value(hasItem(DEFAULT_LOGIN))) + .andExpect(jsonPath("$.[*].firstName").value(hasItem(DEFAULT_FIRSTNAME))) + .andExpect(jsonPath("$.[*].lastName").value(hasItem(DEFAULT_LASTNAME))) + .andExpect(jsonPath("$.[*].email").value(hasItem(DEFAULT_EMAIL))) + .andExpect(jsonPath("$.[*].imageUrl").value(hasItem(DEFAULT_IMAGEURL))) + .andExpect(jsonPath("$.[*].langKey").value(hasItem(DEFAULT_LANGKEY))); + } + + @Test + @Transactional + public void getUser() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + // Get the user + restUserMockMvc.perform(get("/api/users/{login}", user.getLogin())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.login").value(user.getLogin())) + .andExpect(jsonPath("$.firstName").value(DEFAULT_FIRSTNAME)) + .andExpect(jsonPath("$.lastName").value(DEFAULT_LASTNAME)) + .andExpect(jsonPath("$.email").value(DEFAULT_EMAIL)) + .andExpect(jsonPath("$.imageUrl").value(DEFAULT_IMAGEURL)) + .andExpect(jsonPath("$.langKey").value(DEFAULT_LANGKEY)); + } + + @Test + @Transactional + public void getNonExistingUser() throws Exception { + restUserMockMvc.perform(get("/api/users/unknown")) + .andExpect(status().isNotFound()); + } + + @Test + @Transactional + public void updateUser() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeUpdate = userRepository.findAll().size(); + + // Update the user + User updatedUser = userRepository.findOne(user.getId()); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + updatedUser.getId(), + updatedUser.getLogin(), + UPDATED_PASSWORD, + UPDATED_FIRSTNAME, + UPDATED_LASTNAME, + UPDATED_EMAIL, + updatedUser.getActivated(), + UPDATED_IMAGEURL, + UPDATED_LANGKEY, + updatedUser.getCreatedBy(), + updatedUser.getCreatedDate(), + updatedUser.getLastModifiedBy(), + updatedUser.getLastModifiedDate(), + autorities); + + restUserMockMvc.perform(put("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isOk()); + + // Validate the User in the database + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeUpdate); + User testUser = userList.get(userList.size() - 1); + assertThat(testUser.getFirstName()).isEqualTo(UPDATED_FIRSTNAME); + assertThat(testUser.getLastName()).isEqualTo(UPDATED_LASTNAME); + assertThat(testUser.getEmail()).isEqualTo(UPDATED_EMAIL); + assertThat(testUser.getImageUrl()).isEqualTo(UPDATED_IMAGEURL); + assertThat(testUser.getLangKey()).isEqualTo(UPDATED_LANGKEY); + } + + @Test + @Transactional + public void updateUserLogin() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeUpdate = userRepository.findAll().size(); + + // Update the user + User updatedUser = userRepository.findOne(user.getId()); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + updatedUser.getId(), + UPDATED_LOGIN, + UPDATED_PASSWORD, + UPDATED_FIRSTNAME, + UPDATED_LASTNAME, + UPDATED_EMAIL, + updatedUser.getActivated(), + UPDATED_IMAGEURL, + UPDATED_LANGKEY, + updatedUser.getCreatedBy(), + updatedUser.getCreatedDate(), + updatedUser.getLastModifiedBy(), + updatedUser.getLastModifiedDate(), + autorities); + + restUserMockMvc.perform(put("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isOk()); + + // Validate the User in the database + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeUpdate); + User testUser = userList.get(userList.size() - 1); + assertThat(testUser.getLogin()).isEqualTo(UPDATED_LOGIN); + assertThat(testUser.getFirstName()).isEqualTo(UPDATED_FIRSTNAME); + assertThat(testUser.getLastName()).isEqualTo(UPDATED_LASTNAME); + assertThat(testUser.getEmail()).isEqualTo(UPDATED_EMAIL); + assertThat(testUser.getImageUrl()).isEqualTo(UPDATED_IMAGEURL); + assertThat(testUser.getLangKey()).isEqualTo(UPDATED_LANGKEY); + } + + @Test + @Transactional + public void updateUserExistingEmail() throws Exception { + // Initialize the database with 2 users + userRepository.saveAndFlush(user); + + User anotherUser = new User(); + anotherUser.setLogin("jhipster"); + anotherUser.setPassword(RandomStringUtils.random(60)); + anotherUser.setActivated(true); + anotherUser.setEmail("jhipster@localhost"); + anotherUser.setFirstName("java"); + anotherUser.setLastName("hipster"); + anotherUser.setImageUrl(""); + anotherUser.setLangKey("en"); + userRepository.saveAndFlush(anotherUser); + + int databaseSizeBeforeUpdate = userRepository.findAll().size(); + + // Update the user + User updatedUser = userRepository.findOne(user.getId()); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + updatedUser.getId(), + updatedUser.getLogin(), + updatedUser.getPassword(), + updatedUser.getFirstName(), + updatedUser.getLastName(), + "jhipster@localhost", // this email should already be used by anotherUser + updatedUser.getActivated(), + updatedUser.getImageUrl(), + updatedUser.getLangKey(), + updatedUser.getCreatedBy(), + updatedUser.getCreatedDate(), + updatedUser.getLastModifiedBy(), + updatedUser.getLastModifiedDate(), + autorities); + + restUserMockMvc.perform(put("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isBadRequest()); + } + + @Test + @Transactional + public void updateUserExistingLogin() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + User anotherUser = new User(); + anotherUser.setLogin("jhipster"); + anotherUser.setPassword(RandomStringUtils.random(60)); + anotherUser.setActivated(true); + anotherUser.setEmail("jhipster@localhost"); + anotherUser.setFirstName("java"); + anotherUser.setLastName("hipster"); + anotherUser.setImageUrl(""); + anotherUser.setLangKey("en"); + userRepository.saveAndFlush(anotherUser); + int databaseSizeBeforeUpdate = userRepository.findAll().size(); + + // Update the user + User updatedUser = userRepository.findOne(user.getId()); + + Set autorities = new HashSet<>(); + autorities.add("ROLE_USER"); + ManagedUserVM managedUserVM = new ManagedUserVM( + updatedUser.getId(), + "jhipster", // this login should already be used by anotherUser + updatedUser.getPassword(), + updatedUser.getFirstName(), + updatedUser.getLastName(), + updatedUser.getEmail(), + updatedUser.getActivated(), + updatedUser.getImageUrl(), + updatedUser.getLangKey(), + updatedUser.getCreatedBy(), + updatedUser.getCreatedDate(), + updatedUser.getLastModifiedBy(), + updatedUser.getLastModifiedDate(), + autorities); + + restUserMockMvc.perform(put("/api/users") + .contentType(TestUtil.APPLICATION_JSON_UTF8) + .content(TestUtil.convertObjectToJsonBytes(managedUserVM))) + .andExpect(status().isBadRequest()); + } + + @Test + @Transactional + public void deleteUser() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeDelete = userRepository.findAll().size(); + + // Delete the user + restUserMockMvc.perform(delete("/api/users/{login}", user.getLogin()) + .accept(TestUtil.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()); + + // Validate the database is empty + List userList = userRepository.findAll(); + assertThat(userList).hasSize(databaseSizeBeforeDelete - 1); + } + + @Test + @Transactional + public void equalsVerifier() throws Exception { + User userA = new User(); + userA.setLogin("AAA"); + User userB = new User(); + userB.setLogin("BBB"); + assertThat(userA).isNotEqualTo(userB); + } +} diff --git a/jhipster/src/test/javascript/e2e/account/account.spec.ts b/jhipster/src/test/javascript/e2e/account/account.spec.ts new file mode 100644 index 0000000000..cfa9fae078 --- /dev/null +++ b/jhipster/src/test/javascript/e2e/account/account.spec.ts @@ -0,0 +1,108 @@ +import { browser, element, by, $ } from 'protractor'; + +describe('account', () => { + + const username = element(by.id('username')); + const password = element(by.id('password')); + const accountMenu = element(by.id('account-menu')); + const login = element(by.id('login')); + const logout = element(by.id('logout')); + + beforeAll(() => { + browser.get('/'); + }); + + it('should fail to login with bad password', () => { + const expect1 = /home.title/; + element.all(by.css('h1')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + accountMenu.click(); + login.click(); + + username.sendKeys('admin'); + password.sendKeys('foo'); + element(by.css('button[type=submit]')).click(); + + const expect2 = /login.messages.error.authentication/; + element.all(by.css('.alert-danger')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect2); + }); + }); + + it('should login successfully with admin account', () => { + const expect1 = /login.title/; + element.all(by.css('.modal-content h1')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + username.clear(); + username.sendKeys('admin'); + password.clear(); + password.sendKeys('admin'); + element(by.css('button[type=submit]')).click(); + + browser.waitForAngular(); + + const expect2 = /home.logged.message/; + element.all(by.css('.alert-success span')).getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect2); + }); + }); + + it('should be able to update settings', () => { + accountMenu.click(); + element(by.css('[routerLink="settings"]')).click(); + + const expect1 = /settings.title/; + element.all(by.css('h2')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + element(by.css('button[type=submit]')).click(); + + const expect2 = /settings.messages.success/; + element.all(by.css('.alert-success')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect2); + }); + }); + + it('should be able to update password', () => { + accountMenu.click(); + element(by.css('[routerLink="password"]')).click(); + + const expect1 = /password.title/; + element.all(by.css('h2')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + password.sendKeys('newpassword'); + element(by.id('confirmPassword')).sendKeys('newpassword'); + element(by.css('button[type=submit]')).click(); + + const expect2 = /password.messages.success/; + element.all(by.css('.alert-success')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect2); + }); + accountMenu.click(); + logout.click(); + + accountMenu.click(); + login.click(); + + username.sendKeys('admin'); + password.sendKeys('newpassword'); + element(by.css('button[type=submit]')).click(); + + accountMenu.click(); + element(by.css('[routerLink="password"]')).click(); + // change back to default + password.clear(); + password.sendKeys('admin'); + element(by.id('confirmPassword')).clear(); + element(by.id('confirmPassword')).sendKeys('admin'); + element(by.css('button[type=submit]')).click(); + }); + + afterAll(() => { + accountMenu.click(); + logout.click(); + }); +}); diff --git a/jhipster/src/test/javascript/e2e/admin/administration.spec.ts b/jhipster/src/test/javascript/e2e/admin/administration.spec.ts new file mode 100644 index 0000000000..0516f7a27d --- /dev/null +++ b/jhipster/src/test/javascript/e2e/admin/administration.spec.ts @@ -0,0 +1,80 @@ +import { browser, element, by, $ } from 'protractor'; + +describe('administration', () => { + + const username = element(by.id('username')); + const password = element(by.id('password')); + const accountMenu = element(by.id('account-menu')); + const adminMenu = element(by.id('admin-menu')); + const login = element(by.id('login')); + const logout = element(by.id('logout')); + + beforeAll(() => { + browser.get('/'); + + accountMenu.click(); + login.click(); + + username.sendKeys('admin'); + password.sendKeys('admin'); + element(by.css('button[type=submit]')).click(); + browser.waitForAngular(); + }); + + beforeEach(() => { + adminMenu.click(); + }); + + it('should load user management', () => { + element(by.css('[routerLink="user-management"]')).click(); + const expect1 = /userManagement.home.title/; + element.all(by.css('h2 span')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + }); + + it('should load metrics', () => { + element(by.css('[routerLink="jhi-metrics"]')).click(); + const expect1 = /metrics.title/; + element.all(by.css('h2 span')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + }); + + it('should load health', () => { + element(by.css('[routerLink="jhi-health"]')).click(); + const expect1 = /health.title/; + element.all(by.css('h2 span')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + }); + + it('should load configuration', () => { + element(by.css('[routerLink="jhi-configuration"]')).click(); + const expect1 = /configuration.title/; + element.all(by.css('h2')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + }); + + it('should load audits', () => { + element(by.css('[routerLink="audits"]')).click(); + const expect1 = /audits.title/; + element.all(by.css('h2')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + }); + + it('should load logs', () => { + element(by.css('[routerLink="logs"]')).click(); + const expect1 = /logs.title/; + element.all(by.css('h2')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expect1); + }); + }); + + afterAll(() => { + accountMenu.click(); + logout.click(); + }); +}); diff --git a/jhipster/src/test/javascript/e2e/entities/comment.spec.ts b/jhipster/src/test/javascript/e2e/entities/comment.spec.ts new file mode 100644 index 0000000000..3032bd1364 --- /dev/null +++ b/jhipster/src/test/javascript/e2e/entities/comment.spec.ts @@ -0,0 +1,49 @@ +import { browser, element, by, $ } from 'protractor'; + +describe('Comment e2e test', () => { + + const username = element(by.id('username')); + const password = element(by.id('password')); + const entityMenu = element(by.id('entity-menu')); + const accountMenu = element(by.id('account-menu')); + const login = element(by.id('login')); + const logout = element(by.id('logout')); + + beforeAll(() => { + browser.get('/'); + + accountMenu.click(); + login.click(); + + username.sendKeys('admin'); + password.sendKeys('admin'); + element(by.css('button[type=submit]')).click(); + browser.waitForAngular(); + }); + + it('should load Comments', () => { + entityMenu.click(); + element.all(by.css('[routerLink="comment"]')).first().click().then(() => { + const expectVal = /baeldungApp.comment.home.title/; + element.all(by.css('h2 span')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expectVal); + }); + }); + }); + + it('should load create Comment dialog', function () { + element(by.css('button.create-comment')).click().then(() => { + const expectVal = /baeldungApp.comment.home.createOrEditLabel/; + element.all(by.css('h4.modal-title')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expectVal); + }); + + element(by.css('button.close')).click(); + }); + }); + + afterAll(function () { + accountMenu.click(); + logout.click(); + }); +}); diff --git a/jhipster/src/test/javascript/e2e/entities/post.spec.ts b/jhipster/src/test/javascript/e2e/entities/post.spec.ts new file mode 100644 index 0000000000..3c8d04f731 --- /dev/null +++ b/jhipster/src/test/javascript/e2e/entities/post.spec.ts @@ -0,0 +1,49 @@ +import { browser, element, by, $ } from 'protractor'; + +describe('Post e2e test', () => { + + const username = element(by.id('username')); + const password = element(by.id('password')); + const entityMenu = element(by.id('entity-menu')); + const accountMenu = element(by.id('account-menu')); + const login = element(by.id('login')); + const logout = element(by.id('logout')); + + beforeAll(() => { + browser.get('/'); + + accountMenu.click(); + login.click(); + + username.sendKeys('admin'); + password.sendKeys('admin'); + element(by.css('button[type=submit]')).click(); + browser.waitForAngular(); + }); + + it('should load Posts', () => { + entityMenu.click(); + element.all(by.css('[routerLink="post"]')).first().click().then(() => { + const expectVal = /baeldungApp.post.home.title/; + element.all(by.css('h2 span')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expectVal); + }); + }); + }); + + it('should load create Post dialog', function () { + element(by.css('button.create-post')).click().then(() => { + const expectVal = /baeldungApp.post.home.createOrEditLabel/; + element.all(by.css('h4.modal-title')).first().getAttribute('jhiTranslate').then((value) => { + expect(value).toMatch(expectVal); + }); + + element(by.css('button.close')).click(); + }); + }); + + afterAll(function () { + accountMenu.click(); + logout.click(); + }); +}); diff --git a/jhipster/src/test/javascript/karma.conf.js b/jhipster/src/test/javascript/karma.conf.js new file mode 100644 index 0000000000..1b10226955 --- /dev/null +++ b/jhipster/src/test/javascript/karma.conf.js @@ -0,0 +1,126 @@ +'use strict'; + +const path = require('path'); +const webpack = require('webpack'); +const WATCH = process.argv.indexOf('--watch') > -1; +const LoaderOptionsPlugin = require("webpack/lib/LoaderOptionsPlugin"); + +module.exports = function (config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: './', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine', 'intl-shim'], + + // list of files / patterns to load in the browser + files: [ + 'spec/entry.ts' + ], + + + // list of files to exclude + exclude: ['e2e/**'], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'spec/entry.ts': ['webpack', 'sourcemap'] + }, + + webpack: { + resolve: { + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, enforce: 'pre', loader: 'tslint-loader', exclude: /(test|node_modules)/ + }, + { + test: /\.ts$/, + loaders: ['awesome-typescript-loader', 'angular2-template-loader?keepUrl=true'], + exclude: /node_modules/ + }, + { + test: /\.(html|css)$/, + loader: 'raw-loader', + exclude: /\.async\.(html|css)$/ + }, + { + test: /\.async\.(html|css)$/, + loaders: ['file?name=[name].[hash].[ext]', 'extract'] + }, + { + test: /\.scss$/, + loaders: ['to-string-loader', 'css-loader', 'sass-loader'] + }, + { + test: /src\/main\/webapp\/.+\.ts$/, + enforce: 'post', + exclude: /(test|node_modules)/, + loader: 'sourcemap-istanbul-instrumenter-loader?force-sourcemap=true' + }] + }, + devtool: 'inline-source-map', + plugins: [ + new webpack.ContextReplacementPlugin( + // The (\\|\/) piece accounts for path separators in *nix and Windows + /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, + root('./src') // location of your src + ), + new LoaderOptionsPlugin({ + options: { + tslint: { + emitErrors: !WATCH, + failOnHint: false + } + } + }) + ] + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['dots', 'junit', 'progress', 'karma-remap-istanbul'], + + junitReporter: { + outputFile: '../../../../target/test-results/karma/TESTS-results.xml' + }, + + remapIstanbulReporter: { + reports: { // eslint-disable-line + 'html': 'target/test-results/coverage', + 'text-summary': null + } + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: WATCH, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: !WATCH + }); +}; + +function root(__path) { + return path.join(__dirname, __path); +} diff --git a/jhipster/src/test/javascript/protractor.conf.js b/jhipster/src/test/javascript/protractor.conf.js new file mode 100644 index 0000000000..e79b09e17b --- /dev/null +++ b/jhipster/src/test/javascript/protractor.conf.js @@ -0,0 +1,48 @@ +var HtmlScreenshotReporter = require("protractor-jasmine2-screenshot-reporter"); +var JasmineReporters = require('jasmine-reporters'); + +exports.config = { + allScriptsTimeout: 20000, + + specs: [ + './e2e/account/*.spec.ts', + './e2e/admin/*.spec.ts', + './e2e/entities/*.spec.ts' + ], + + capabilities: { + 'browserName': 'chrome', + 'phantomjs.binary.path': require('phantomjs-prebuilt').path, + 'phantomjs.ghostdriver.cli.args': ['--loglevel=DEBUG'] + }, + + directConnect: true, + + baseUrl: 'http://localhost:8080/', + + framework: 'jasmine2', + + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000 + }, + + beforeLaunch: function() { + require('ts-node').register({ + project: '' + }); + }, + + onPrepare: function() { + browser.driver.manage().window().setSize(1280, 1024); + jasmine.getEnv().addReporter(new JasmineReporters.JUnitXmlReporter({ + savePath: 'target/reports/e2e', + consolidateAll: false + })); + jasmine.getEnv().addReporter(new HtmlScreenshotReporter({ + dest: "target/reports/e2e/screenshots" + })); + }, + + useAllAngular2AppRoots: true +}; diff --git a/jhipster/src/test/javascript/spec/app/account/activate/activate.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/activate/activate.component.spec.ts new file mode 100644 index 0000000000..76a6e9f941 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/activate/activate.component.spec.ts @@ -0,0 +1,84 @@ +import { TestBed, async, tick, fakeAsync, inject } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs/Rx'; +import { BaeldungTestModule } from '../../../test.module'; +import { MockActivatedRoute } from '../../../helpers/mock-route.service'; +import { LoginModalService } from '../../../../../../main/webapp/app/shared'; +import { Activate } from '../../../../../../main/webapp/app/account/activate/activate.service'; +import { ActivateComponent } from '../../../../../../main/webapp/app/account/activate/activate.component'; + +describe('Component Tests', () => { + + describe('ActivateComponent', () => { + + let comp: ActivateComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [ActivateComponent], + providers: [ + Activate, + { + provide: ActivatedRoute, + useValue: new MockActivatedRoute({'key': 'ABC123'}) + }, + { + provide: LoginModalService, + useValue: null + } + ] + }).overrideComponent(ActivateComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + let fixture = TestBed.createComponent(ActivateComponent); + comp = fixture.componentInstance; + }); + + it('calls activate.get with the key from params', + inject([Activate], + fakeAsync((service: Activate) => { + spyOn(service, 'get').and.returnValue(Observable.of()); + + comp.ngOnInit(); + tick(); + + expect(service.get).toHaveBeenCalledWith('ABC123'); + }) + ) + ); + + it('should set set success to OK upon successful activation', + inject([Activate], + fakeAsync((service: Activate) => { + spyOn(service, 'get').and.returnValue(Observable.of({})); + + comp.ngOnInit(); + tick(); + + expect(comp.error).toBe(null); + expect(comp.success).toEqual('OK'); + }) + ) + ); + + it('should set set error to ERROR upon activation failure', + inject([Activate], + fakeAsync((service: Activate) => { + spyOn(service, 'get').and.returnValue(Observable.throw('ERROR')); + + comp.ngOnInit(); + tick(); + + expect(comp.error).toBe('ERROR'); + expect(comp.success).toEqual(null); + }) + ) + ); + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/account/password-reset/finish/password-reset-finish.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/password-reset/finish/password-reset-finish.component.spec.ts new file mode 100644 index 0000000000..537c58351e --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/password-reset/finish/password-reset-finish.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed, inject } from '@angular/core/testing'; +import { Renderer, ElementRef } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { LoginModalService } from '../../../../../../../main/webapp/app/shared'; +import { Observable } from 'rxjs/Rx'; +import { BaeldungTestModule } from '../../../../test.module'; +import { PasswordResetFinishComponent } from '../../../../../../../main/webapp/app/account/password-reset/finish/password-reset-finish.component'; +import { PasswordResetFinish } from '../../../../../../../main/webapp/app/account/password-reset/finish/password-reset-finish.service'; +import { MockActivatedRoute } from '../../../../helpers/mock-route.service'; + + +describe('Component Tests', () => { + + describe('PasswordResetFinishComponent', () => { + + let fixture: ComponentFixture; + let comp: PasswordResetFinishComponent; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [PasswordResetFinishComponent], + providers: [ + PasswordResetFinish, + { + provide: LoginModalService, + useValue: null + }, + { + provide: ActivatedRoute, + useValue: new MockActivatedRoute({'key': 'XYZPDQ'}) + }, + { + provide: Renderer, + useValue: { + invokeElementMethod(renderElement: any, methodName: string, args?: any[]) {} + } + }, + { + provide: ElementRef, + useValue: new ElementRef(null) + } + ] + }).overrideComponent(PasswordResetFinishComponent, { + set: { + template: '' + } + }).createComponent(PasswordResetFinishComponent); + comp = fixture.componentInstance; + }); + + it('should define its initial state', function () { + comp.ngOnInit(); + + expect(comp.keyMissing).toBeFalsy(); + expect(comp.key).toEqual('XYZPDQ'); + expect(comp.resetAccount).toEqual({}); + }); + + it('sets focus after the view has been initialized', + inject([ElementRef], (elementRef: ElementRef) => { + let element = fixture.nativeElement; + let node = { + focus() {} + }; + + elementRef.nativeElement = element; + spyOn(element, 'querySelector').and.returnValue(node); + spyOn(node, 'focus'); + + comp.ngAfterViewInit(); + + expect(element.querySelector).toHaveBeenCalledWith('#password'); + expect(node.focus).toHaveBeenCalled(); + }) + ); + + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/account/password-reset/init/password-reset-init.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/password-reset/init/password-reset-init.component.spec.ts new file mode 100644 index 0000000000..55c0a81922 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/password-reset/init/password-reset-init.component.spec.ts @@ -0,0 +1,115 @@ +import { ComponentFixture, TestBed, inject } from '@angular/core/testing'; +import { Renderer, ElementRef } from '@angular/core'; +import { Observable } from 'rxjs/Rx'; +import { BaeldungTestModule } from '../../../../test.module'; +import { PasswordResetInitComponent } from '../../../../../../../main/webapp/app/account/password-reset/init/password-reset-init.component'; +import { PasswordResetInit } from '../../../../../../../main/webapp/app/account/password-reset/init/password-reset-init.service'; + + +describe('Component Tests', () => { + + describe('PasswordResetInitComponent', function () { + let fixture: ComponentFixture; + let comp: PasswordResetInitComponent; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [PasswordResetInitComponent], + providers: [ + PasswordResetInit, + { + provide: Renderer, + useValue: { + invokeElementMethod(renderElement: any, methodName: string, args?: any[]) {} + } + }, + { + provide: ElementRef, + useValue: new ElementRef(null) + } + ] + }).overrideComponent(PasswordResetInitComponent, { + set: { + template: '' + } + }).createComponent(PasswordResetInitComponent); + comp = fixture.componentInstance; + comp.ngOnInit(); + }); + + it('should define its initial state', function () { + expect(comp.success).toBeUndefined(); + expect(comp.error).toBeUndefined(); + expect(comp.errorEmailNotExists).toBeUndefined(); + expect(comp.resetAccount).toEqual({}); + }); + + it('sets focus after the view has been initialized', + inject([ElementRef], (elementRef: ElementRef) => { + let element = fixture.nativeElement; + let node = { + focus() {} + }; + + elementRef.nativeElement = element; + spyOn(element, 'querySelector').and.returnValue(node); + spyOn(node, 'focus'); + + comp.ngAfterViewInit(); + + expect(element.querySelector).toHaveBeenCalledWith('#email'); + expect(node.focus).toHaveBeenCalled(); + }) + ); + + it('notifies of success upon successful requestReset', + inject([PasswordResetInit], (service: PasswordResetInit) => { + spyOn(service, 'save').and.returnValue(Observable.of({})); + comp.resetAccount.email = 'user@domain.com'; + + comp.requestReset(); + + expect(service.save).toHaveBeenCalledWith('user@domain.com'); + expect(comp.success).toEqual('OK'); + expect(comp.error).toBeNull(); + expect(comp.errorEmailNotExists).toBeNull(); + }) + ); + + it('notifies of unknown email upon e-mail address not registered/400', + inject([PasswordResetInit], (service: PasswordResetInit) => { + spyOn(service, 'save').and.returnValue(Observable.throw({ + status: 400, + data: 'e-mail address not registered' + })); + comp.resetAccount.email = 'user@domain.com'; + + comp.requestReset(); + + expect(service.save).toHaveBeenCalledWith('user@domain.com'); + expect(comp.success).toBeNull(); + expect(comp.error).toBeNull(); + expect(comp.errorEmailNotExists).toEqual('ERROR'); + }) + ); + + it('notifies of error upon error response', + inject([PasswordResetInit], (service: PasswordResetInit) => { + spyOn(service, 'save').and.returnValue(Observable.throw({ + status: 503, + data: 'something else' + })); + comp.resetAccount.email = 'user@domain.com'; + + comp.requestReset(); + + expect(service.save).toHaveBeenCalledWith('user@domain.com'); + expect(comp.success).toBeNull(); + expect(comp.errorEmailNotExists).toBeNull(); + expect(comp.error).toEqual('ERROR'); + }) + ); + + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/account/password/password-strength-bar.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/password/password-strength-bar.component.spec.ts new file mode 100644 index 0000000000..9cdc55529c --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/password/password-strength-bar.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +import { PasswordStrengthBarComponent } from '../../../../../../main/webapp/app/account/password/password-strength-bar.component'; + +describe('Component Tests', () => { + + describe('PasswordStrengthBarComponent', () => { + + let comp: PasswordStrengthBarComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [PasswordStrengthBarComponent] + }).overrideComponent(PasswordStrengthBarComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordStrengthBarComponent); + comp = fixture.componentInstance; + }); + + describe('PasswordStrengthBarComponents', () => { + it('should initialize with default values', () => { + expect(comp.measureStrength('')).toBe(0); + expect(comp.colors).toEqual(['#F00', '#F90', '#FF0', '#9F0', '#0F0']); + expect(comp.getColor(0).idx).toBe(1); + expect(comp.getColor(0).col).toBe(comp.colors[0]); + }); + + it('should increase strength upon password value change', () => { + expect(comp.measureStrength('')).toBe(0); + expect(comp.measureStrength('aa')).toBeGreaterThanOrEqual(comp.measureStrength('')); + expect(comp.measureStrength('aa^6')).toBeGreaterThanOrEqual(comp.measureStrength('aa')); + expect(comp.measureStrength('Aa090(**)')).toBeGreaterThanOrEqual(comp.measureStrength('aa^6')); + expect(comp.measureStrength('Aa090(**)+-07365')).toBeGreaterThanOrEqual(comp.measureStrength('Aa090(**)')); + }); + + it('should change the color based on strength', () => { + expect(comp.getColor(0).col).toBe(comp.colors[0]); + expect(comp.getColor(11).col).toBe(comp.colors[1]); + expect(comp.getColor(22).col).toBe(comp.colors[2]); + expect(comp.getColor(33).col).toBe(comp.colors[3]); + expect(comp.getColor(44).col).toBe(comp.colors[4]); + }); + }); + }); +}); + diff --git a/jhipster/src/test/javascript/spec/app/account/password/password.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/password/password.component.spec.ts new file mode 100644 index 0000000000..e6f4983785 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/password/password.component.spec.ts @@ -0,0 +1,92 @@ +import { ComponentFixture, TestBed, async, inject } from '@angular/core/testing'; +import { Observable } from 'rxjs/Rx'; +import { BaeldungTestModule } from '../../../test.module'; +import { PasswordComponent } from '../../../../../../main/webapp/app/account/password/password.component'; +import { Password } from '../../../../../../main/webapp/app/account/password/password.service'; +import { Principal } from '../../../../../../main/webapp/app/shared/auth/principal.service'; +import { AccountService } from '../../../../../../main/webapp/app/shared/auth/account.service'; + + +describe('Component Tests', () => { + + describe('PasswordComponent', () => { + + let comp: PasswordComponent; + let fixture: ComponentFixture; + let service: Password; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [PasswordComponent], + providers: [ + Principal, + AccountService, + Password + ] + }).overrideComponent(PasswordComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordComponent); + comp = fixture.componentInstance; + service = fixture.debugElement.injector.get(Password); + }); + + it('should show error if passwords do not match', () => { + // GIVEN + comp.password = 'password1'; + comp.confirmPassword = 'password2'; + // WHEN + comp.changePassword(); + // THEN + expect(comp.doNotMatch).toBe('ERROR'); + expect(comp.error).toBeNull(); + expect(comp.success).toBeNull(); + }); + + it('should call Auth.changePassword when passwords match', () => { + // GIVEN + spyOn(service, 'save').and.returnValue(Observable.of(true)); + comp.password = comp.confirmPassword = 'myPassword'; + + // WHEN + comp.changePassword(); + + // THEN + expect(service.save).toHaveBeenCalledWith('myPassword'); + }); + + it('should set success to OK upon success', function() { + // GIVEN + spyOn(service, 'save').and.returnValue(Observable.of(true)); + comp.password = comp.confirmPassword = 'myPassword'; + + // WHEN + comp.changePassword(); + + // THEN + expect(comp.doNotMatch).toBeNull(); + expect(comp.error).toBeNull(); + expect(comp.success).toBe('OK'); + }); + + it('should notify of error if change password fails', function() { + // GIVEN + spyOn(service, 'save').and.returnValue(Observable.throw('ERROR')); + comp.password = comp.confirmPassword = 'myPassword'; + + // WHEN + comp.changePassword(); + + // THEN + expect(comp.doNotMatch).toBeNull(); + expect(comp.success).toBeNull(); + expect(comp.error).toBe('ERROR'); + }); + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/account/register/register.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/register/register.component.spec.ts new file mode 100644 index 0000000000..c475c2f3d2 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/register/register.component.spec.ts @@ -0,0 +1,138 @@ +import { ComponentFixture, TestBed, async, inject, tick, fakeAsync } from '@angular/core/testing'; +import { Renderer, ElementRef } from '@angular/core'; +import { Observable } from 'rxjs/Rx'; +import { JhiLanguageService } from 'ng-jhipster'; +import { MockLanguageService } from '../../../helpers/mock-language.service'; +import { BaeldungTestModule } from '../../../test.module'; +import { LoginModalService } from '../../../../../../main/webapp/app/shared'; +import { Register } from '../../../../../../main/webapp/app/account/register/register.service'; +import { RegisterComponent } from '../../../../../../main/webapp/app/account/register/register.component'; + + +describe('Component Tests', () => { + + describe('RegisterComponent', () => { + let fixture: ComponentFixture; + let comp: RegisterComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [RegisterComponent], + providers: [ + Register, + { + provide: LoginModalService, + useValue: null + }, + { + provide: Renderer, + useValue: null + }, + { + provide: ElementRef, + useValue: null + } + ] + }).overrideComponent(RegisterComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterComponent); + comp = fixture.componentInstance; + comp.ngOnInit(); + }); + + it('should ensure the two passwords entered match', function () { + comp.registerAccount.password = 'password'; + comp.confirmPassword = 'non-matching'; + + comp.register(); + + expect(comp.doNotMatch).toEqual('ERROR'); + }); + + it('should update success to OK after creating an account', + inject([Register, JhiLanguageService], + fakeAsync((service: Register, mockTranslate: MockLanguageService) => { + spyOn(service, 'save').and.returnValue(Observable.of({})); + comp.registerAccount.password = comp.confirmPassword = 'password'; + + comp.register(); + tick(); + + expect(service.save).toHaveBeenCalledWith({ + password: 'password', + langKey: 'en' + }); + expect(comp.success).toEqual(true); + expect(comp.registerAccount.langKey).toEqual('en'); + expect(mockTranslate.getCurrentSpy).toHaveBeenCalled(); + expect(comp.errorUserExists).toBeNull(); + expect(comp.errorEmailExists).toBeNull(); + expect(comp.error).toBeNull(); + }) + ) + ); + + it('should notify of user existence upon 400/login already in use', + inject([Register], + fakeAsync((service: Register) => { + spyOn(service, 'save').and.returnValue(Observable.throw({ + status: 400, + _body: 'login already in use' + })); + comp.registerAccount.password = comp.confirmPassword = 'password'; + + comp.register(); + tick(); + + expect(comp.errorUserExists).toEqual('ERROR'); + expect(comp.errorEmailExists).toBeNull(); + expect(comp.error).toBeNull(); + }) + ) + ); + + it('should notify of email existence upon 400/e-mail address already in use', + inject([Register], + fakeAsync((service: Register) => { + spyOn(service, 'save').and.returnValue(Observable.throw({ + status: 400, + _body: 'e-mail address already in use' + })); + comp.registerAccount.password = comp.confirmPassword = 'password'; + + comp.register(); + tick(); + + expect(comp.errorEmailExists).toEqual('ERROR'); + expect(comp.errorUserExists).toBeNull(); + expect(comp.error).toBeNull(); + }) + ) + ); + + it('should notify of generic error', + inject([Register], + fakeAsync((service: Register) => { + spyOn(service, 'save').and.returnValue(Observable.throw({ + status: 503 + })); + comp.registerAccount.password = comp.confirmPassword = 'password'; + + comp.register(); + tick(); + + expect(comp.errorUserExists).toBeNull(); + expect(comp.errorEmailExists).toBeNull(); + expect(comp.error).toEqual('ERROR'); + }) + ) + ); + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/account/settings/settings.component.spec.ts b/jhipster/src/test/javascript/spec/app/account/settings/settings.component.spec.ts new file mode 100644 index 0000000000..266a33be79 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/account/settings/settings.component.spec.ts @@ -0,0 +1,103 @@ +import { ComponentFixture, TestBed, async, inject } from '@angular/core/testing'; +import { Observable } from 'rxjs/Rx'; +import { JhiLanguageHelper } from '../../../../../../main/webapp/app/shared'; +import { BaeldungTestModule } from '../../../test.module'; +import { Principal, AccountService } from '../../../../../../main/webapp/app/shared'; +import { SettingsComponent } from '../../../../../../main/webapp/app/account/settings/settings.component'; +import { MockAccountService } from '../../../helpers/mock-account.service'; +import { MockPrincipal } from '../../../helpers/mock-principal.service'; + + +describe('Component Tests', () => { + + describe('SettingsComponent', () => { + + let comp: SettingsComponent; + let fixture: ComponentFixture; + let mockAuth: MockAccountService; + let mockPrincipal: MockPrincipal; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [SettingsComponent], + providers: [ + { + provide: Principal, + useClass: MockPrincipal + }, + { + provide: AccountService, + useClass: MockAccountService + }, + { + provide: JhiLanguageHelper, + useValue: null + }, + ] + }).overrideComponent(SettingsComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsComponent); + comp = fixture.componentInstance; + mockAuth = fixture.debugElement.injector.get(AccountService); + mockPrincipal = fixture.debugElement.injector.get(Principal); + }); + + it('should send the current identity upon save', function () { + // GIVEN + let accountValues = { + firstName: 'John', + lastName: 'Doe', + + activated: true, + email: 'john.doe@mail.com', + langKey: 'en', + login: 'john' + }; + mockPrincipal.setResponse(accountValues); + + // WHEN + comp.settingsAccount = accountValues; + comp.save(); + + // THEN + expect(mockPrincipal.identitySpy).toHaveBeenCalled(); + expect(mockAuth.saveSpy).toHaveBeenCalledWith(accountValues); + expect(comp.settingsAccount).toEqual(accountValues); + }); + + it('should notify of success upon successful save', function () { + // GIVEN + let accountValues = { + firstName: 'John', + lastName: 'Doe' + }; + mockPrincipal.setResponse(accountValues); + + // WHEN + comp.save(); + + // THEN + expect(comp.error).toBeNull(); + expect(comp.success).toBe('OK'); + }); + + it('should notify of error upon failed save', function () { + // GIVEN + mockAuth.saveSpy.and.returnValue(Observable.throw('ERROR')); + + // WHEN + comp.save(); + + // THEN + expect(comp.error).toEqual('ERROR'); + expect(comp.success).toBeNull(); + }); + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/admin/audits/audits.component.spec.ts b/jhipster/src/test/javascript/spec/app/admin/audits/audits.component.spec.ts new file mode 100644 index 0000000000..d16673de03 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/admin/audits/audits.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { DatePipe } from '@angular/common'; +import { NgbPaginationConfig} from '@ng-bootstrap/ng-bootstrap'; +import { ParseLinks } from 'ng-jhipster'; +import { BaeldungTestModule } from '../../../test.module'; +import { PaginationConfig } from '../../../../../../main/webapp/app/blocks/config/uib-pagination.config' +import { AuditsComponent } from '../../../../../../main/webapp/app/admin/audits/audits.component'; +import { AuditsService } from '../../../../../../main/webapp/app/admin/audits/audits.service'; +import { ITEMS_PER_PAGE } from '../../../../../../main/webapp/app/shared'; + + +function getDate(isToday= true){ + let date: Date = new Date(); + if (isToday) { + // Today + 1 day - needed if the current day must be included + date.setDate(date.getDate() + 1); + return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + } + return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; +} + +describe('Component Tests', () => { + + describe('AuditsComponent', () => { + + let comp: AuditsComponent; + let fixture: ComponentFixture; + let service: AuditsService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [AuditsComponent], + providers: [ + AuditsService, + NgbPaginationConfig, + ParseLinks, + PaginationConfig, + DatePipe + ] + }) + .overrideComponent(AuditsComponent, { + set: { + template: '' + } + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AuditsComponent); + comp = fixture.componentInstance; + service = fixture.debugElement.injector.get(AuditsService); + }); + + describe('today function ', () => { + it('should set toDate to current date', () => { + comp.today(); + expect(comp.toDate).toBe(getDate()); + }); + }); + + describe('previousMonth function ', () => { + it('should set toDate to current date', () => { + comp.previousMonth(); + expect(comp.fromDate).toBe(getDate(false)); + }); + }); + + describe('By default, on init', () => { + it('should set all default values correctly', () => { + fixture.detectChanges(); + expect(comp.toDate).toBe(getDate()); + expect(comp.fromDate).toBe(getDate(false)); + expect(comp.itemsPerPage).toBe(ITEMS_PER_PAGE); + expect(comp.page).toBe(1); + expect(comp.reverse).toBeFalsy(); + expect(comp.orderProp).toBe('timestamp'); + }); + }); + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/admin/health/health.component.spec.ts b/jhipster/src/test/javascript/spec/app/admin/health/health.component.spec.ts new file mode 100644 index 0000000000..b80c96db66 --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/admin/health/health.component.spec.ts @@ -0,0 +1,295 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { BaeldungTestModule } from '../../../test.module'; +import { JhiHealthCheckComponent } from '../../../../../../main/webapp/app/admin/health/health.component'; +import { JhiHealthService } from '../../../../../../main/webapp/app/admin/health/health.service'; + + +describe('Component Tests', () => { + + describe('JhiHealthCheckComponent', () => { + + let comp: JhiHealthCheckComponent; + let fixture: ComponentFixture; + let service: JhiHealthService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [BaeldungTestModule], + declarations: [JhiHealthCheckComponent], + providers: [ + JhiHealthService, + { + provide: NgbModal, + useValue: null + } + ] + }) + .overrideComponent(JhiHealthCheckComponent, { + set: { + template: '' + } + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(JhiHealthCheckComponent); + comp = fixture.componentInstance; + service = fixture.debugElement.injector.get(JhiHealthService); + }); + + describe('baseName and subSystemName', () => { + it('should return the basename when it has no sub system', () => { + expect(comp.baseName('base')).toBe('base'); + }); + + it('should return the basename when it has sub systems', () => { + expect(comp.baseName('base.subsystem.system')).toBe('base'); + }); + + it('should return the sub system name', () => { + expect(comp.subSystemName('subsystem')).toBe(''); + }); + + it('should return the subsystem when it has multiple keys', () => { + expect(comp.subSystemName('subsystem.subsystem.system')).toBe(' - subsystem.system'); + }); + }); + + describe('transformHealthData', () => { + it('should flatten empty health data', () => { + const data = {}; + const expected = []; + expect(service.transformHealthData(data)).toEqual(expected); + }); + }); + + it('should flatten health data with no subsystems', () => { + const data = { + 'status': 'UP', + 'db': { + 'status': 'UP', + 'database': 'H2', + 'hello': '1' + }, + 'mail': { + 'status': 'UP', + 'error': 'mail.a.b.c' + } + }; + const expected = [ + { + 'name': 'db', + 'error': undefined, + 'status': 'UP', + 'details': { + 'database': 'H2', + 'hello': '1' + } + }, + { + 'name': 'mail', + 'error': 'mail.a.b.c', + 'status': 'UP' + } + ]; + expect(service.transformHealthData(data)).toEqual(expected); + }); + + it('should flatten health data with subsystems at level 1, main system has no additional information', () => { + const data = { + 'status': 'UP', + 'db': { + 'status': 'UP', + 'database': 'H2', + 'hello': '1' + }, + 'mail': { + 'status': 'UP', + 'error': 'mail.a.b.c' + }, + 'system': { + 'status': 'DOWN', + 'subsystem1': { + 'status': 'UP', + 'property1': 'system.subsystem1.property1' + }, + 'subsystem2': { + 'status': 'DOWN', + 'error': 'system.subsystem1.error', + 'property2': 'system.subsystem2.property2' + } + } + }; + const expected = [ + { + 'name': 'db', + 'error': undefined, + 'status': 'UP', + 'details': { + 'database': 'H2', + 'hello': '1' + } + }, + { + 'name': 'mail', + 'error': 'mail.a.b.c', + 'status': 'UP' + }, + { + 'name': 'system.subsystem1', + 'error': undefined, + 'status': 'UP', + 'details': { + 'property1': 'system.subsystem1.property1' + } + }, + { + 'name': 'system.subsystem2', + 'error': 'system.subsystem1.error', + 'status': 'DOWN', + 'details': { + 'property2': 'system.subsystem2.property2' + } + } + ]; + expect(service.transformHealthData(data)).toEqual(expected); + }); + + it('should flatten health data with subsystems at level 1, main system has additional information', () => { + const data = { + 'status': 'UP', + 'db': { + 'status': 'UP', + 'database': 'H2', + 'hello': '1' + }, + 'mail': { + 'status': 'UP', + 'error': 'mail.a.b.c' + }, + 'system': { + 'status': 'DOWN', + 'property1': 'system.property1', + 'subsystem1': { + 'status': 'UP', + 'property1': 'system.subsystem1.property1' + }, + 'subsystem2': { + 'status': 'DOWN', + 'error': 'system.subsystem1.error', + 'property2': 'system.subsystem2.property2' + } + } + }; + const expected = [ + { + 'name': 'db', + 'error': undefined, + 'status': 'UP', + 'details': { + 'database': 'H2', + 'hello': '1' + } + }, + { + 'name': 'mail', + 'error': 'mail.a.b.c', + 'status': 'UP' + }, + { + 'name': 'system', + 'error': undefined, + 'status': 'DOWN', + 'details': { + 'property1': 'system.property1' + } + }, + { + 'name': 'system.subsystem1', + 'error': undefined, + 'status': 'UP', + 'details': { + 'property1': 'system.subsystem1.property1' + } + }, + { + 'name': 'system.subsystem2', + 'error': 'system.subsystem1.error', + 'status': 'DOWN', + 'details': { + 'property2': 'system.subsystem2.property2' + } + } + ]; + expect(service.transformHealthData(data)).toEqual(expected); + }); + + it('should flatten health data with subsystems at level 1, main system has additional error', () => { + const data = { + 'status': 'UP', + 'db': { + 'status': 'UP', + 'database': 'H2', + 'hello': '1' + }, + 'mail': { + 'status': 'UP', + 'error': 'mail.a.b.c' + }, + 'system': { + 'status': 'DOWN', + 'error': 'show me', + 'subsystem1': { + 'status': 'UP', + 'property1': 'system.subsystem1.property1' + }, + 'subsystem2': { + 'status': 'DOWN', + 'error': 'system.subsystem1.error', + 'property2': 'system.subsystem2.property2' + } + } + }; + const expected = [ + { + 'name': 'db', + 'error': undefined, + 'status': 'UP', + 'details': { + 'database': 'H2', + 'hello': '1' + } + }, + { + 'name': 'mail', + 'error': 'mail.a.b.c', + 'status': 'UP' + }, + { + 'name': 'system', + 'error': 'show me', + 'status': 'DOWN' + }, + { + 'name': 'system.subsystem1', + 'error': undefined, + 'status': 'UP', + 'details': { + 'property1': 'system.subsystem1.property1' + } + }, + { + 'name': 'system.subsystem2', + 'error': 'system.subsystem1.error', + 'status': 'DOWN', + 'details': { + 'property2': 'system.subsystem2.property2' + } + } + ]; + expect(service.transformHealthData(data)).toEqual(expected); + }); + }); +}); diff --git a/jhipster/src/test/javascript/spec/app/entities/comment/comment-detail.component.spec.ts b/jhipster/src/test/javascript/spec/app/entities/comment/comment-detail.component.spec.ts new file mode 100644 index 0000000000..b7c6b77b8c --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/entities/comment/comment-detail.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed, async, inject } from '@angular/core/testing'; +import { MockBackend } from '@angular/http/testing'; +import { Http, BaseRequestOptions } from '@angular/http'; +import { OnInit } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs/Rx'; +import { DateUtils, DataUtils } from 'ng-jhipster'; +import { JhiLanguageService } from 'ng-jhipster'; +import { MockLanguageService } from '../../../helpers/mock-language.service'; +import { MockActivatedRoute } from '../../../helpers/mock-route.service'; +import { CommentDetailComponent } from '../../../../../../main/webapp/app/entities/comment/comment-detail.component'; +import { CommentService } from '../../../../../../main/webapp/app/entities/comment/comment.service'; +import { Comment } from '../../../../../../main/webapp/app/entities/comment/comment.model'; + +describe('Component Tests', () => { + + describe('Comment Management Detail Component', () => { + let comp: CommentDetailComponent; + let fixture: ComponentFixture; + let service: CommentService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CommentDetailComponent], + providers: [ + MockBackend, + BaseRequestOptions, + DateUtils, + DataUtils, + DatePipe, + { + provide: ActivatedRoute, + useValue: new MockActivatedRoute({id: 123}) + }, + { + provide: Http, + useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => { + return new Http(backendInstance, defaultOptions); + }, + deps: [MockBackend, BaseRequestOptions] + }, + { + provide: JhiLanguageService, + useClass: MockLanguageService + }, + CommentService + ] + }).overrideComponent(CommentDetailComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommentDetailComponent); + comp = fixture.componentInstance; + service = fixture.debugElement.injector.get(CommentService); + }); + + + describe('OnInit', () => { + it('Should call load all on init', () => { + // GIVEN + + spyOn(service, 'find').and.returnValue(Observable.of(new Comment(10))); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(comp.comment).toEqual(jasmine.objectContaining({id:10})); + }); + }); + }); + +}); diff --git a/jhipster/src/test/javascript/spec/app/entities/post/post-detail.component.spec.ts b/jhipster/src/test/javascript/spec/app/entities/post/post-detail.component.spec.ts new file mode 100644 index 0000000000..3ccb9cf6ad --- /dev/null +++ b/jhipster/src/test/javascript/spec/app/entities/post/post-detail.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed, async, inject } from '@angular/core/testing'; +import { MockBackend } from '@angular/http/testing'; +import { Http, BaseRequestOptions } from '@angular/http'; +import { OnInit } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs/Rx'; +import { DateUtils, DataUtils } from 'ng-jhipster'; +import { JhiLanguageService } from 'ng-jhipster'; +import { MockLanguageService } from '../../../helpers/mock-language.service'; +import { MockActivatedRoute } from '../../../helpers/mock-route.service'; +import { PostDetailComponent } from '../../../../../../main/webapp/app/entities/post/post-detail.component'; +import { PostService } from '../../../../../../main/webapp/app/entities/post/post.service'; +import { Post } from '../../../../../../main/webapp/app/entities/post/post.model'; + +describe('Component Tests', () => { + + describe('Post Management Detail Component', () => { + let comp: PostDetailComponent; + let fixture: ComponentFixture; + let service: PostService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [PostDetailComponent], + providers: [ + MockBackend, + BaseRequestOptions, + DateUtils, + DataUtils, + DatePipe, + { + provide: ActivatedRoute, + useValue: new MockActivatedRoute({id: 123}) + }, + { + provide: Http, + useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => { + return new Http(backendInstance, defaultOptions); + }, + deps: [MockBackend, BaseRequestOptions] + }, + { + provide: JhiLanguageService, + useClass: MockLanguageService + }, + PostService + ] + }).overrideComponent(PostDetailComponent, { + set: { + template: '' + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PostDetailComponent); + comp = fixture.componentInstance; + service = fixture.debugElement.injector.get(PostService); + }); + + + describe('OnInit', () => { + it('Should call load all on init', () => { + // GIVEN + + spyOn(service, 'find').and.returnValue(Observable.of(new Post(10))); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(comp.post).toEqual(jasmine.objectContaining({id:10})); + }); + }); + }); + +}); diff --git a/jhipster/src/test/javascript/spec/entry.ts b/jhipster/src/test/javascript/spec/entry.ts new file mode 100644 index 0000000000..64edbafb93 --- /dev/null +++ b/jhipster/src/test/javascript/spec/entry.ts @@ -0,0 +1,19 @@ +/// +import 'core-js'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/proxy'; +import 'zone.js/dist/jasmine-patch'; +import 'rxjs'; +import 'intl/locale-data/jsonp/en-US.js'; +import { TestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +declare let require: any; +const testsContext: any = require.context('./', true, /\.spec/); +testsContext.keys().forEach(testsContext); diff --git a/jhipster/src/test/javascript/spec/helpers/mock-account.service.ts b/jhipster/src/test/javascript/spec/helpers/mock-account.service.ts new file mode 100644 index 0000000000..e21c10a370 --- /dev/null +++ b/jhipster/src/test/javascript/spec/helpers/mock-account.service.ts @@ -0,0 +1,26 @@ +import { SpyObject } from './spyobject'; +import { AccountService } from '../../../../main/webapp/app/shared/auth/account.service'; +import Spy = jasmine.Spy; + +export class MockAccountService extends SpyObject { + + getSpy: Spy; + saveSpy: Spy; + fakeResponse: any; + + constructor() { + super(AccountService); + + this.fakeResponse = null; + this.getSpy = this.spy('get').andReturn(this); + this.saveSpy = this.spy('save').andReturn(this); + } + + subscribe(callback: any) { + callback(this.fakeResponse); + } + + setResponse(json: any): void { + this.fakeResponse = json; + } +} diff --git a/jhipster/src/test/javascript/spec/helpers/mock-language.service.ts b/jhipster/src/test/javascript/spec/helpers/mock-language.service.ts new file mode 100644 index 0000000000..0c7dec92f1 --- /dev/null +++ b/jhipster/src/test/javascript/spec/helpers/mock-language.service.ts @@ -0,0 +1,26 @@ +import { SpyObject } from './spyobject'; +import { JhiLanguageService } from 'ng-jhipster'; +import Spy = jasmine.Spy; + +export class MockLanguageService extends SpyObject { + + getCurrentSpy: Spy; + fakeResponse: any; + + constructor() { + super(JhiLanguageService); + + this.fakeResponse = 'en'; + this.getCurrentSpy = this.spy('getCurrent').andReturn(Promise.resolve(this.fakeResponse)); + } + + init() {} + + changeLanguage(languageKey: string) {} + + setLocations(locations: string[]) {} + + addLocation(location: string) {} + + reload() {} +} diff --git a/jhipster/src/test/javascript/spec/helpers/mock-principal.service.ts b/jhipster/src/test/javascript/spec/helpers/mock-principal.service.ts new file mode 100644 index 0000000000..89b932b83c --- /dev/null +++ b/jhipster/src/test/javascript/spec/helpers/mock-principal.service.ts @@ -0,0 +1,20 @@ +import { SpyObject } from './spyobject'; +import { Principal } from '../../../../main/webapp/app/shared/auth/principal.service'; +import Spy = jasmine.Spy; + +export class MockPrincipal extends SpyObject { + + identitySpy: Spy; + fakeResponse: any; + + constructor() { + super(Principal); + + this.fakeResponse = {}; + this.identitySpy = this.spy('identity').andReturn(Promise.resolve(this.fakeResponse)); + } + + setResponse(json: any): void { + this.fakeResponse = json; + } +} diff --git a/jhipster/src/test/javascript/spec/helpers/mock-route.service.ts b/jhipster/src/test/javascript/spec/helpers/mock-route.service.ts new file mode 100644 index 0000000000..3ddb291721 --- /dev/null +++ b/jhipster/src/test/javascript/spec/helpers/mock-route.service.ts @@ -0,0 +1,15 @@ +import { ActivatedRoute, Params } from '@angular/router'; +import { Observable } from 'rxjs'; + +export class MockActivatedRoute extends ActivatedRoute { + + constructor(parameters?: any) { + super(); + this.queryParams = Observable.of(parameters); + this.params = Observable.of(parameters); + } +} + +export class MockRouter { + navigate = jasmine.createSpy('navigate'); +} diff --git a/jhipster/src/test/javascript/spec/helpers/spyobject.ts b/jhipster/src/test/javascript/spec/helpers/spyobject.ts new file mode 100644 index 0000000000..4db41fb8df --- /dev/null +++ b/jhipster/src/test/javascript/spec/helpers/spyobject.ts @@ -0,0 +1,69 @@ +export interface GuinessCompatibleSpy extends jasmine.Spy { + /** By chaining the spy with and.returnValue, all calls to the function will return a specific + * value. */ + andReturn(val: any): void; + /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied + * function. */ + andCallFake(fn: Function): GuinessCompatibleSpy; + /** removes all recorded calls */ + reset(); +} + +export class SpyObject { + static stub(object = null, config = null, overrides = null) { + if (!(object instanceof SpyObject)) { + overrides = config; + config = object; + object = new SpyObject(); + } + + let m = {}; + Object.keys(config).forEach((key) => m[key] = config[key]); + Object.keys(overrides).forEach((key) => m[key] = overrides[key]); + Object.keys(m).forEach((key) => { + object.spy(key).andReturn(m[key]); + }); + return object; + } + + constructor(type = null) { + if (type) { + Object.keys(type.prototype).forEach((prop) => { + let m = null; + try { + m = type.prototype[prop]; + } catch (e) { + // As we are creating spys for abstract classes, + // these classes might have getters that throw when they are accessed. + // As we are only auto creating spys for methods, this + // should not matter. + } + if (typeof m === 'function') { + this.spy(prop); + } + }); + } + } + + spy(name) { + if (!this[name]) { + this[name] = this._createGuinnessCompatibleSpy(name); + } + return this[name]; + } + + prop(name, value) { + this[name] = value; + } + + /** @internal */ + _createGuinnessCompatibleSpy(name): GuinessCompatibleSpy { + let newSpy: GuinessCompatibleSpy = < any > jasmine.createSpy(name); + newSpy.andCallFake = < any > newSpy.and.callFake; + newSpy.andReturn = < any > newSpy.and.returnValue; + newSpy.reset = < any > newSpy.calls.reset; + // revisit return null here (previously needed for rtts_assert). + newSpy.and.returnValue(null); + return newSpy; + } +} diff --git a/jhipster/src/test/javascript/spec/test.module.ts b/jhipster/src/test/javascript/spec/test.module.ts new file mode 100644 index 0000000000..65ce439cb0 --- /dev/null +++ b/jhipster/src/test/javascript/spec/test.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { MockBackend } from '@angular/http/testing'; +import { Http, BaseRequestOptions } from '@angular/http'; +import { JhiLanguageService } from 'ng-jhipster'; +import { MockLanguageService } from './helpers/mock-language.service'; + +@NgModule({ + providers: [ + MockBackend, + BaseRequestOptions, + { + provide: JhiLanguageService, + useClass: MockLanguageService + }, + { + provide: Http, + useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => { + return new Http(backendInstance, defaultOptions); + }, + deps: [MockBackend, BaseRequestOptions] + } + ] +}) +export class BaeldungTestModule {} diff --git a/jhipster/src/test/resources/config/application.yml b/jhipster/src/test/resources/config/application.yml new file mode 100644 index 0000000000..a7939c838c --- /dev/null +++ b/jhipster/src/test/resources/config/application.yml @@ -0,0 +1,96 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration is used for unit/integration tests. +# +# More information on profiles: https://jhipster.github.io/profiles/ +# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + + +spring: + application: + name: baeldung + jackson: + serialization.write_dates_as_timestamps: false + cache: + type: none + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:h2:mem:baeldung;DB_CLOSE_DELAY=-1 + name: + username: + password: + jpa: + database-platform: io.github.jhipster.domain.util.FixedH2Dialect + database: H2 + open-in-view: false + show-sql: true + hibernate: + ddl-auto: none + naming: + physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + properties: + hibernate.id.new_generator_mappings: true + hibernate.cache.use_second_level_cache: false + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: true + hibernate.hbm2ddl.auto: validate + mail: + host: localhost + messages: + basename: i18n/messages + mvc: + favicon: + enabled: false + thymeleaf: + mode: XHTML + +liquibase: + contexts: test + +security: + basic: + enabled: false + +server: + port: 10344 + address: localhost + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# =================================================================== + +jhipster: + async: + core-pool-size: 2 + max-pool-size: 50 + queue-capacity: 10000 + security: + authentication: + jwt: + secret: e1d4b69d3f953e3fa622121e882e6f459ca20ca4 + # Token is valid 24 hours + token-validity-in-seconds: 86400 + metrics: # DropWizard Metrics configuration, used by MetricsConfiguration + jmx.enabled: true + +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://jhipster.github.io/common-application-properties/ +# =================================================================== + +application: diff --git a/jhipster/src/test/resources/logback-test.xml b/jhipster/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..c0acd00401 --- /dev/null +++ b/jhipster/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/jhipster/tsconfig.json b/jhipster/tsconfig.json new file mode 100644 index 0000000000..354ae048ad --- /dev/null +++ b/jhipster/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "removeComments": false, + "noImplicitAny": false, + "suppressImplicitAnyIndexErrors": true, + "outDir": "target/www/app", + "lib": ["es6", "dom"], + "typeRoots": [ + "node_modules/@types" + ] + }, + "include": [ + "src/main/webapp/app", + "src/test/javascript" + ] +} \ No newline at end of file diff --git a/jhipster/tslint.json b/jhipster/tslint.json new file mode 100644 index 0000000000..ee6491cf69 --- /dev/null +++ b/jhipster/tslint.json @@ -0,0 +1,107 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "indent": [ + true, + "spaces" + ], + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + "static-before-instance", + "variables-before-functions" + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-inferrable-types": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + + "directive-selector": [true, "attribute", "jhi", "camelCase"], + "component-selector": [true, "element", "jhi", "kebab-case"], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": false, + "component-class-suffix": true, + "directive-class-suffix": true, + "no-access-missing-member": true, + "templates-use-public": true, + "invoke-injectable": true + } +} diff --git a/jhipster/webpack/webpack.common.js b/jhipster/webpack/webpack.common.js new file mode 100644 index 0000000000..4916bb2db5 --- /dev/null +++ b/jhipster/webpack/webpack.common.js @@ -0,0 +1,117 @@ +const webpack = require('webpack'); +const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const StringReplacePlugin = require('string-replace-webpack-plugin'); +const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); +const path = require('path'); + +module.exports = function (options) { + const DATAS = { + VERSION: JSON.stringify(require("../package.json").version), + DEBUG_INFO_ENABLED: options.env === 'dev' + }; + return { + entry: { + 'polyfills': './src/main/webapp/app/polyfills', + 'global': './src/main/webapp/content/scss/global.scss', + 'main': './src/main/webapp/app/app.main' + }, + resolve: { + extensions: ['.ts', '.js'], + modules: ['node_modules'] + }, + module: { + rules: [ + { test: /bootstrap\/dist\/js\/umd\//, loader: 'imports-loader?jQuery=jquery' }, + { + test: /\.ts$/, + loaders: [ + 'angular2-template-loader', + 'awesome-typescript-loader' + ], + exclude: ['node_modules/generator-jhipster'] + }, + { + test: /\.html$/, + loader: 'raw-loader', + exclude: ['./src/main/webapp/index.html'] + }, + { + test: /\.scss$/, + loaders: ['to-string-loader', 'css-loader', 'sass-loader'], + exclude: /(vendor\.scss|global\.scss)/ + }, + { + test: /(vendor\.scss|global\.scss)/, + loaders: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] + }, + { + test: /\.css$/, + loaders: ['to-string-loader', 'css-loader'], + exclude: /(vendor\.css|global\.css)/ + }, + { + test: /(vendor\.css|global\.css)/, + loaders: ['style-loader', 'css-loader'] + }, + { + test: /\.(jpe?g|png|gif|svg|woff|woff2|ttf|eot)$/i, + loaders: [ + 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]', { + loader: 'image-webpack-loader', + query: { + gifsicle: { + interlaced: false + }, + optipng: { + optimizationLevel: 7 + } + } + } + ] + }, + { + test: /app.constants.ts$/, + loader: StringReplacePlugin.replace({ + replacements: [{ + pattern: /\/\* @toreplace (\w*?) \*\//ig, + replacement: function (match, p1, offset, string) { + return `_${p1} = ${DATAS[p1]};`; + } + }] + }) + } + ] + }, + plugins: [ + new CommonsChunkPlugin({ + names: ['manifest', 'polyfills'].reverse() + }), + new webpack.DllReferencePlugin({ + context: './', + manifest: require(path.resolve('./target/www/vendor.json')), + }), + new CopyWebpackPlugin([ + { from: './node_modules/swagger-ui/dist', to: 'swagger-ui/dist' }, + { from: './src/main/webapp/swagger-ui/', to: 'swagger-ui' }, + { from: './src/main/webapp/favicon.ico', to: 'favicon.ico' }, + { from: './src/main/webapp/robots.txt', to: 'robots.txt' }, + { from: './src/main/webapp/i18n', to: 'i18n' } + ]), + new webpack.ProvidePlugin({ + $: "jquery", + jQuery: "jquery" + }), + new HtmlWebpackPlugin({ + template: './src/main/webapp/index.html', + chunksSortMode: 'dependency', + inject: 'body' + }), + new AddAssetHtmlPlugin([ + { filepath: path.resolve('./target/www/vendor.dll.js'), includeSourcemap: false } + ]), + new StringReplacePlugin() + ] + }; +}; diff --git a/jhipster/webpack/webpack.dev.js b/jhipster/webpack/webpack.dev.js new file mode 100644 index 0000000000..f612a44b09 --- /dev/null +++ b/jhipster/webpack/webpack.dev.js @@ -0,0 +1,65 @@ +const webpack = require('webpack'); +const path = require('path'); +const commonConfig = require('./webpack.common.js'); +const writeFilePlugin = require('write-file-webpack-plugin'); +const webpackMerge = require('webpack-merge'); +const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); +const ExtractTextPlugin = require("extract-text-webpack-plugin"); +const ENV = 'dev'; +const execSync = require('child_process').execSync; +const fs = require('fs'); +const ddlPath = './target/www/vendor.json'; + +if (!fs.existsSync(ddlPath)) { + execSync('webpack --config webpack/webpack.vendor.js'); +} + +module.exports = webpackMerge(commonConfig({ env: ENV }), { + devtool: 'inline-source-map', + devServer: { + contentBase: './target/www', + proxy: [{ + context: [ + '/api', + '/management', + '/swagger-resources', + '/v2/api-docs', + '/h2-console' + ], + target: 'http://127.0.0.1:8080', + secure: false + }] + }, + output: { + path: path.resolve('target/www'), + filename: '[name].bundle.js', + chunkFilename: '[id].chunk.js' + }, + module: { + rules: [{ + test: /\.ts$/, + loaders: [ + 'tslint-loader' + ], + exclude: ['node_modules', new RegExp('reflect-metadata\\' + path.sep + 'Reflect\\.ts')] + }] + }, + plugins: [ + new BrowserSyncPlugin({ + host: 'localhost', + port: 9000, + proxy: { + target: 'http://localhost:9060' + } + }, { + reload: false + }), + new ExtractTextPlugin('styles.css'), + new webpack.NoEmitOnErrorsPlugin(), + new webpack.NamedModulesPlugin(), + new writeFilePlugin(), + new webpack.WatchIgnorePlugin([ + path.resolve('./src/test'), + ]) + ] +}); diff --git a/jhipster/webpack/webpack.prod.js b/jhipster/webpack/webpack.prod.js new file mode 100644 index 0000000000..28f0f2152b --- /dev/null +++ b/jhipster/webpack/webpack.prod.js @@ -0,0 +1,22 @@ +const commonConfig = require('./webpack.common.js'); +const webpackMerge = require('webpack-merge'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const ExtractTextPlugin = require("extract-text-webpack-plugin"); +const Visualizer = require('webpack-visualizer-plugin'); +const ENV = 'prod'; + +module.exports = webpackMerge(commonConfig({ env: ENV }), { + devtool: 'source-map', + output: { + path: './target/www', + filename: '[hash].[name].bundle.js', + chunkFilename: '[hash].[id].chunk.js' + }, + plugins: [ + new ExtractTextPlugin('[hash].styles.css'), + new Visualizer({ + // Webpack statistics in target folder + filename: '../stats.html' + }) + ] +}); diff --git a/jhipster/webpack/webpack.vendor.js b/jhipster/webpack/webpack.vendor.js new file mode 100644 index 0000000000..449024d102 --- /dev/null +++ b/jhipster/webpack/webpack.vendor.js @@ -0,0 +1,63 @@ +var webpack = require('webpack'); +module.exports = { + entry: { + 'vendor': [ + './src/main/webapp/app/vendor', + '@angular/common', + '@angular/compiler', + '@angular/core', + '@angular/forms', + '@angular/http', + '@angular/platform-browser', + '@angular/platform-browser-dynamic', + '@angular/router', + '@ng-bootstrap/ng-bootstrap', + 'angular2-cookie', + 'angular2-infinite-scroll', + 'jquery', + 'ng-jhipster', + 'ng2-webstorage', + 'rxjs' + ] + }, + resolve: { + extensions: ['.ts', '.js'], + modules: ['node_modules'] + }, + module: { + exprContextCritical: false, + rules: [ + { + test: /(vendor\.scss|global\.scss)/, + loaders: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] + }, + { + test: /\.(jpe?g|png|gif|svg|woff|woff2|ttf|eot)$/i, + loaders: [ + 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]', { + loader: 'image-webpack-loader', + query: { + gifsicle: { + interlaced: false + }, + optipng: { + optimizationLevel: 7 + } + } + } + ] + } + ] + }, + output: { + filename: '[name].dll.js', + path: './target/www', + library: '[name]' + }, + plugins: [ + new webpack.DllPlugin({ + name: '[name]', + path: './target/www/[name].json' + }) + ] +}; diff --git a/mockito2/src/test/java/com/baeldung/mockito/java8/ArgumentMatcherWithoutLambdaUnitTest.java b/mockito2/src/test/java/com/baeldung/mockito/java8/ArgumentMatcherWithoutLambdaUnitTest.java index 786062ee57..aaa8d03585 100644 --- a/mockito2/src/test/java/com/baeldung/mockito/java8/ArgumentMatcherWithoutLambdaUnitTest.java +++ b/mockito2/src/test/java/com/baeldung/mockito/java8/ArgumentMatcherWithoutLambdaUnitTest.java @@ -13,6 +13,16 @@ import static org.mockito.Mockito.when; public class ArgumentMatcherWithoutLambdaUnitTest { + private class PeterArgumentMatcher implements ArgumentMatcher { + + @Override + public boolean matches(Person p) { + return p + .getName() + .equals("Peter"); + } + } + @InjectMocks private UnemploymentServiceImpl unemploymentService; @@ -34,16 +44,6 @@ public class ArgumentMatcherWithoutLambdaUnitTest { assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter)); } - private class PeterArgumentMatcher implements ArgumentMatcher { - - @Override - public boolean matches(Person p) { - return p - .getName() - .equals("Peter"); - } - } - @Before public void init() { MockitoAnnotations.initMocks(this); diff --git a/mockito2/src/test/java/com/baeldung/mockito/java8/CustomAnswerWithoutLambdaUnitTest.java b/mockito2/src/test/java/com/baeldung/mockito/java8/CustomAnswerWithoutLambdaUnitTest.java index 9d1aa3a3c0..d5b9d6d1ce 100644 --- a/mockito2/src/test/java/com/baeldung/mockito/java8/CustomAnswerWithoutLambdaUnitTest.java +++ b/mockito2/src/test/java/com/baeldung/mockito/java8/CustomAnswerWithoutLambdaUnitTest.java @@ -17,7 +17,21 @@ import static org.mockito.Mockito.when; public class CustomAnswerWithoutLambdaUnitTest { + + private class PersonAnswer implements Answer> { + @Override + public Stream answer(InvocationOnMock invocation) throws Throwable { + Person person = invocation.getArgument(0); + + if(person.getName().equals("Peter")) { + return Stream.builder().add(new JobPosition("Teacher")).build(); + } + + return Stream.empty(); + } + } + @InjectMocks private UnemploymentServiceImpl unemploymentService; @@ -37,17 +51,6 @@ public class CustomAnswerWithoutLambdaUnitTest { assertFalse(unemploymentService.searchJob(linda, "").isPresent()); } - - private class PersonAnswer implements Answer> { - - @Override - public Stream answer(InvocationOnMock invocation) throws Throwable { - Person person = invocation.getArgument(0); - - return Stream.of(new JobPosition("Teacher")) - .filter(p -> person.getName().equals("Peter")); - } - } @Before public void init() { diff --git a/pom.xml b/pom.xml index e0556a7c50..bc89f839eb 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ javaxval jaxb jee7 + jhipster jjwt jooq jpa-storedprocedure